use gur::prelude::*;
use gur::snapshot::Snapshot;
use gur::ur::{Ur, UrBuilder};
use std::cmp;
use std::fs::File;
use std::io;
use std::io::{BufRead, BufReader, BufWriter, Read, Write};
use std::path::PathBuf;
#[derive(Clone, Default)]
struct TextBuffer(Vec<char>);
impl TextBuffer {
fn new<'a>(buffer: Vec<char>) -> Ur<'a, Self, String> {
UrBuilder::default().build(TextBuffer(buffer))
}
fn get(&self) -> &[char] {
&self.0
}
fn insert(&mut self, pos: usize, text: &str) {
let pos = cmp::max(0, cmp::min(pos, self.0.len()));
self.0.splice(pos..pos, text.chars());
}
fn delete(&mut self, pos: usize, count: usize) {
let begin = cmp::max(0, cmp::min(pos, self.0.len()));
let end = cmp::max(0, cmp::min(pos + count, self.0.len()));
self.0.drain(begin..end);
}
}
impl Snapshot for TextBuffer {
type Snapshot = String;
fn to_snapshot(&self) -> Self::Snapshot {
self.0.iter().collect()
}
fn from_snapshot(snapshot: &Self::Snapshot) -> Self {
Self(snapshot.chars().collect())
}
}
struct TextEditor<'a> {
buffer: Ur<'a, TextBuffer, String>,
cursor: usize,
file: Option<PathBuf>,
}
impl<'a> TextEditor<'a> {
fn new() -> Self {
Self {
buffer: TextBuffer::new(Vec::new()),
cursor: 0,
file: None,
}
}
fn open(&mut self, path: Option<PathBuf>) -> io::Result<()> {
if let Some(path) = path {
let path = PathBuf::from(path);
let mut s = String::new();
if let Ok(f) = File::open(&path) {
io::BufReader::new(f).read_to_string(&mut s)?;
}
self.buffer = TextBuffer::new(s.chars().collect());
self.cursor = 0;
self.file = Some(path);
} else {
*self = Self::new();
}
self.set_cursor_pos(self.cursor);
Ok(())
}
fn save(&self, path: Option<PathBuf>) -> io::Result<()> {
let path = if let Some(_) = path {
&path
} else {
&self.file
};
if path.is_none() {
return Ok(());
}
let mut file = io::BufWriter::new(File::create(path.as_ref().unwrap())?);
self.write(&mut file)?;
Ok(())
}
fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
let s: String = (*self.buffer).get().iter().collect();
writer.write_all(s.as_bytes())?;
Ok(())
}
fn print<W: Write>(&self, writer: &mut W) -> io::Result<()> {
let s: String = (*self.buffer).get()[0..self.cursor].iter().collect();
writer.write_all(s.as_bytes())?;
write!(writer, "|")?;
let s: String = (*self.buffer).get()[self.cursor..].iter().collect();
writer.write_all(s.as_bytes())?;
Ok(())
}
fn seek(&mut self, count: isize) {
let pos = if 0 < count {
self.cursor.saturating_add(count as usize)
} else {
self.cursor.saturating_sub(-count as usize)
};
self.set_cursor_pos(pos);
}
fn insert(&mut self, text: String) {
let pos = self.cursor;
self.buffer.edit(move |mut buf| {
buf.insert(pos, &text);
buf
});
self.set_cursor_pos(self.cursor);
}
fn delete(&mut self, count: usize) {
let pos = self.cursor;
self.buffer.edit(move |mut buf| {
buf.delete(pos, count);
buf
});
self.set_cursor_pos(self.cursor);
}
fn undo(&mut self, count: usize) {
for _ in 0..count {
self.buffer.undo();
}
self.set_cursor_pos(self.cursor);
}
fn redo(&mut self, count: usize) {
for _ in 0..count {
self.buffer.redo();
}
self.set_cursor_pos(self.cursor);
}
fn set_cursor_pos(&mut self, pos: usize) {
self.cursor = cmp::min(pos, (*self.buffer).get().len());
}
}
const COMMAND_HELP: &str = "COMMANDS
:h | Print command help.
:p | Print the current text buffer and cursor position.
:s [COUNT] | Seek the cursor +COUNT characters.
:i TEXT | Insert TEXT at the cursor position.
:d [COUNT] | Delete COUNT bytes from the cursor.
:u [COUNT] | Undo COUNT changes.
:r [COUNT] | Redo COUNT changes.
:o [FILE] | Open FILE.
:w [FILE] | Save the current text buffer as FILE.
:q | Quit the program.";
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("Simple text editor.\n\n{}", COMMAND_HELP);
let mut app = TextEditor::new();
let mut stdout = BufWriter::new(io::stdout().lock());
let mut stdin = BufReader::new(io::stdin().lock());
let mut command_str = String::new();
loop {
command_str.clear();
write!(&mut stdout, ":")?;
stdout.flush()?;
if 0 == stdin.read_line(&mut command_str)? {
return Ok(());
}
match Command::parse(&command_str)? {
Command::Help => writeln!(&mut stdout, "{}", COMMAND_HELP)?,
Command::Print => {
app.print(&mut stdout)?;
write!(&mut stdout, "\n")?;
stdout.flush()?;
}
Command::Open(srcpath) => {
app.open(srcpath)?;
}
Command::Write(dstpath) => {
app.save(dstpath)?;
}
Command::Quit => return Ok(()),
Command::Seek(count) => app.seek(count),
Command::Insert(text) => app.insert(text),
Command::Delete(count) => app.delete(count),
Command::Undo(count) => app.undo(count),
Command::Redo(count) => app.redo(count),
}
}
}
enum Command {
Help,
Print,
Seek(isize),
Insert(String),
Delete(usize),
Undo(usize),
Redo(usize),
Write(Option<PathBuf>),
Open(Option<PathBuf>),
Quit,
}
impl Command {
fn parse(command_str: &str) -> Result<Command, Box<dyn std::error::Error>> {
let command_str = command_str
.trim_start_matches(Self::is_whitespace)
.trim_end_matches(&['\r', '\n']);
let (name, param) = if let Some((name, param)) = command_str.split_once(Self::is_whitespace)
{
(name, param)
} else {
(command_str, "")
};
match name {
"h" => Ok(Command::Help),
"p" => Ok(Command::Print),
"q" => Ok(Command::Quit),
"s" => Self::parse_seek(param),
"i" => Self::parse_insert(param),
"d" => Self::parse_delete(param),
"u" => Self::parse_undo(param),
"r" => Self::parse_redo(param),
"o" => Self::parse_open(param),
"w" => Self::parse_write(param),
_ => Ok(Command::Help),
}
}
fn parse_seek(param: &str) -> Result<Command, Box<dyn std::error::Error>> {
let count = Self::parse_isize(param)?;
Ok(Command::Seek(count.unwrap_or(0)))
}
fn parse_insert(param: &str) -> Result<Command, Box<dyn std::error::Error>> {
Ok(Command::Insert(param.to_string()))
}
fn parse_delete(param: &str) -> Result<Command, Box<dyn std::error::Error>> {
let count = Self::parse_usize(param)?;
Ok(Command::Delete(count.unwrap_or(1)))
}
fn parse_undo(param: &str) -> Result<Command, Box<dyn std::error::Error>> {
let count = Self::parse_usize(param)?;
Ok(Command::Undo(count.unwrap_or(1)))
}
fn parse_redo(param: &str) -> Result<Command, Box<dyn std::error::Error>> {
let count = Self::parse_usize(param)?;
Ok(Command::Redo(count.unwrap_or(1)))
}
fn parse_open(param: &str) -> Result<Command, Box<dyn std::error::Error>> {
Ok(Command::Open(Self::parse_path(param)?))
}
fn parse_write(param: &str) -> Result<Command, Box<dyn std::error::Error>> {
Ok(Command::Write(Self::parse_path(param)?))
}
fn parse_path(param: &str) -> Result<Option<PathBuf>, Box<dyn std::error::Error>> {
let path = if let Some(_) = param.find(|c: char| !Self::is_whitespace(c)) {
Some(PathBuf::from(param))
} else {
None
};
Ok(path)
}
fn parse_isize(param: &str) -> Result<Option<isize>, Box<dyn std::error::Error>> {
if let None = param.find(|c: char| !Self::is_whitespace(c)) {
return Ok(None);
}
let num = param.parse::<isize>()?;
Ok(Some(num))
}
fn parse_usize(param: &str) -> Result<Option<usize>, Box<dyn std::error::Error>> {
if let None = param.find(|c: char| !Self::is_whitespace(c)) {
return Ok(None);
}
let num = param.parse::<usize>()?;
Ok(Some(num))
}
fn is_whitespace(c: char) -> bool {
c.is_ascii_whitespace()
}
}