feat: add editing buffer capabilities
This commit is contained in:
parent
24572fd293
commit
41fe3b8864
35
Cargo.lock
generated
35
Cargo.lock
generated
@ -50,6 +50,8 @@ name = "hecto"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"crossterm",
|
||||
"termion",
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -89,6 +91,12 @@ dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "numtoa"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.1"
|
||||
@ -121,6 +129,15 @@ dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_termios"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8440d8acb4fd3d277125b4bd01a6f38aee8d814b3b5fc09b3f2b825d37d3fe8f"
|
||||
dependencies = [
|
||||
"redox_syscall",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.1.0"
|
||||
@ -163,6 +180,24 @@ version = "1.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1"
|
||||
|
||||
[[package]]
|
||||
name = "termion"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "659c1f379f3408c7e5e84c7d0da6d93404e3800b6b9d063ba24436419302ec90"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"numtoa",
|
||||
"redox_syscall",
|
||||
"redox_termios",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.0+wasi-snapshot-preview1"
|
||||
|
@ -7,3 +7,5 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
crossterm = "0.24.0"
|
||||
termion = "2.0.1"
|
||||
unicode-segmentation = "1.10.0"
|
||||
|
81
src/document.rs
Normal file
81
src/document.rs
Normal file
@ -0,0 +1,81 @@
|
||||
use std::fs;
|
||||
|
||||
use crate::{Position, Row};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Document {
|
||||
rows: Vec<Row>,
|
||||
pub file_name: Option<String>,
|
||||
}
|
||||
|
||||
impl Document {
|
||||
pub fn open(filename: &str) -> Result<Self, std::io::Error> {
|
||||
let contents = fs::read_to_string(filename)?;
|
||||
let mut rows = Vec::new();
|
||||
for value in contents.lines() {
|
||||
rows.push(Row::from(value));
|
||||
}
|
||||
Ok(Self {
|
||||
rows,
|
||||
file_name: Some(filename.to_string()),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn row(&self, index: usize) -> Option<&Row> {
|
||||
self.rows.get(index)
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.rows.is_empty()
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.rows.len()
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, at: &Position, c: char) {
|
||||
if c == '\n' {
|
||||
self.insert_newline(at);
|
||||
return;
|
||||
}
|
||||
|
||||
if at.y == self.len() {
|
||||
let mut row = Row::default();
|
||||
row.insert(0, c);
|
||||
self.rows.push(row);
|
||||
} else if at.y < self.len() {
|
||||
let row = self.rows.get_mut(at.y).unwrap();
|
||||
row.insert(at.x, c);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_newline(&mut self, at: &Position) {
|
||||
if at.y > self.len() {
|
||||
return;
|
||||
}
|
||||
|
||||
if at.y == self.len() {
|
||||
self.rows.push(Row::default());
|
||||
return;
|
||||
}
|
||||
|
||||
let new_row = self.rows.get_mut(at.y).unwrap().split(at.x);
|
||||
self.rows.insert(at.y + 1, new_row);
|
||||
}
|
||||
|
||||
pub fn delete(&mut self, at: &Position) {
|
||||
let len = self.len();
|
||||
if at.y >= len {
|
||||
return;
|
||||
}
|
||||
|
||||
if at.x == self.rows.get_mut(at.y).unwrap().len() && at.y < len - 1 {
|
||||
let next_row = self.rows.remove(at.y + 1);
|
||||
let row = self.rows.get_mut(at.y).unwrap();
|
||||
row.append(&next_row);
|
||||
} else {
|
||||
let row = self.rows.get_mut(at.y).unwrap();
|
||||
row.delete(at.x);
|
||||
}
|
||||
}
|
||||
}
|
284
src/editor.rs
284
src/editor.rs
@ -1,12 +1,46 @@
|
||||
use std::io::{self, Write, stdout};
|
||||
use std::env;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
use crossterm::{
|
||||
event::{read, Event, KeyCode},
|
||||
terminal::{self, ClearType}, queue,
|
||||
};
|
||||
use crossterm::terminal;
|
||||
use termion::color;
|
||||
use termion::event::Key;
|
||||
|
||||
use crate::Document;
|
||||
use crate::Row;
|
||||
use crate::Terminal;
|
||||
|
||||
const STATUS_FG_COLOR: color::Rgb = color::Rgb(63, 63, 63);
|
||||
const STATUS_BG_COLOR: color::Rgb = color::Rgb(239, 239, 239);
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Position {
|
||||
pub x: usize,
|
||||
pub y: usize,
|
||||
}
|
||||
|
||||
pub struct StatusMessage {
|
||||
text: String,
|
||||
time: Instant,
|
||||
}
|
||||
|
||||
impl StatusMessage {
|
||||
fn from(message: String) -> Self {
|
||||
Self {
|
||||
time: Instant::now(),
|
||||
text: message,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Editor {
|
||||
should_quit: bool,
|
||||
terminal: Terminal,
|
||||
cursor_position: Position,
|
||||
offset: Position,
|
||||
document: Document,
|
||||
status_message: StatusMessage,
|
||||
}
|
||||
|
||||
impl Editor {
|
||||
@ -17,7 +51,7 @@ impl Editor {
|
||||
if let Err(error) = self.refresh_screen() {
|
||||
die(error);
|
||||
}
|
||||
|
||||
|
||||
if self.should_quit {
|
||||
terminal::disable_raw_mode();
|
||||
break;
|
||||
@ -30,38 +64,244 @@ impl Editor {
|
||||
}
|
||||
|
||||
fn process_keypress(&mut self) -> Result<(), std::io::Error> {
|
||||
let pressed_key = read_key()?;
|
||||
let pressed_key = Terminal::read_key()?;
|
||||
match pressed_key {
|
||||
KeyCode::Char('q') => self.should_quit = true,
|
||||
Key::Ctrl('q') => self.should_quit = true,
|
||||
Key::Char(c) => {
|
||||
self.document.insert(&self.cursor_position, c);
|
||||
self.move_cursor(Key::Right);
|
||||
}
|
||||
Key::Delete => self.document.delete(&self.cursor_position),
|
||||
Key::Backspace => {
|
||||
if self.cursor_position.x > 0 || self.cursor_position.y > 0 {
|
||||
self.move_cursor(Key::Left);
|
||||
self.document.delete(&self.cursor_position);
|
||||
}
|
||||
}
|
||||
Key::Up
|
||||
| Key::Down
|
||||
| Key::Left
|
||||
| Key::Right
|
||||
| Key::PageUp
|
||||
| Key::PageDown
|
||||
| Key::Home
|
||||
| Key::End => self.move_cursor(pressed_key),
|
||||
_ => (),
|
||||
}
|
||||
self.scroll();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn scroll(&mut self) {
|
||||
let Position { x, y } = self.cursor_position;
|
||||
let width = self.terminal.size().width as usize;
|
||||
let height = self.terminal.size().height as usize;
|
||||
let mut offset = &mut self.offset;
|
||||
|
||||
if y < offset.y {
|
||||
offset.y = y;
|
||||
} else if y >= offset.y.saturating_add(height) {
|
||||
offset.y = y.saturating_sub(height).saturating_add(1);
|
||||
}
|
||||
|
||||
if x < offset.x {
|
||||
offset.x = x;
|
||||
} else if x > offset.x.saturating_add(width) {
|
||||
offset.x = x.saturating_sub(width).saturating_add(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn move_cursor(&mut self, key: Key) {
|
||||
let terminal_height = self.terminal.size().height as usize;
|
||||
let Position { mut y, mut x } = self.cursor_position;
|
||||
let height = self.document.len();
|
||||
let mut width = if let Some(row) = self.document.row(y) {
|
||||
row.len()
|
||||
} else {
|
||||
0
|
||||
};
|
||||
if x > width {
|
||||
x = width;
|
||||
}
|
||||
|
||||
match key {
|
||||
Key::Up => y = y.saturating_sub(1),
|
||||
Key::Down => {
|
||||
if y < height {
|
||||
y = y.saturating_add(1);
|
||||
}
|
||||
}
|
||||
Key::Left => {
|
||||
if x > 0 {
|
||||
x -= 1;
|
||||
} else if y > 0 {
|
||||
y -= 1;
|
||||
if let Some(row) = self.document.row(y) {
|
||||
x = row.len();
|
||||
} else {
|
||||
x = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
Key::Right => {
|
||||
if x < width {
|
||||
x += 1;
|
||||
} else {
|
||||
y += 1;
|
||||
x = 0;
|
||||
}
|
||||
}
|
||||
Key::PageUp => {
|
||||
y = if y > terminal_height {
|
||||
y - terminal_height
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
Key::PageDown => {
|
||||
y = if y.saturating_add(terminal_height) < height {
|
||||
y + terminal_height as usize
|
||||
} else {
|
||||
height
|
||||
}
|
||||
}
|
||||
Key::Home => x = 0,
|
||||
Key::End => x = width,
|
||||
_ => (),
|
||||
}
|
||||
|
||||
width = if let Some(row) = self.document.row(y) {
|
||||
row.len()
|
||||
} else {
|
||||
0
|
||||
};
|
||||
if x > width {
|
||||
x = width;
|
||||
}
|
||||
|
||||
self.cursor_position = Position { x, y }
|
||||
}
|
||||
|
||||
fn refresh_screen(&self) -> Result<(), std::io::Error> {
|
||||
let mut stdout = stdout();
|
||||
queue!(stdout, terminal::Clear(ClearType::All));
|
||||
stdout.flush();
|
||||
Terminal::cursor_hide();
|
||||
Terminal::cursor_position(&Position::default());
|
||||
|
||||
if self.should_quit {
|
||||
Terminal::clear_screen();
|
||||
println!("Goodbye.\r");
|
||||
} else {
|
||||
self.draw_rows();
|
||||
self.draw_status_bar();
|
||||
self.draw_message_bar();
|
||||
Terminal::cursor_position(&Position {
|
||||
x: self.cursor_position.x.saturating_sub(self.offset.x),
|
||||
y: self.cursor_position.y.saturating_sub(self.offset.y),
|
||||
});
|
||||
}
|
||||
Terminal::cursor_show();
|
||||
Terminal::flush();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn default() -> Self {
|
||||
Self { should_quit: false }
|
||||
fn draw_welcome_message(&self) {
|
||||
let mut welcome_message = format!("Hecto Editor -- Version {}\r", VERSION);
|
||||
let width = self.terminal.size().width as usize;
|
||||
let len = welcome_message.len();
|
||||
let padding = width.saturating_sub(len) / 2;
|
||||
let spaces = " ".repeat(padding.saturating_sub(1));
|
||||
welcome_message = format!("~{}{}", spaces, welcome_message);
|
||||
welcome_message.truncate(width);
|
||||
println!("{}\r", welcome_message);
|
||||
}
|
||||
}
|
||||
|
||||
fn read_key() -> Result<KeyCode, std::io::Error> {
|
||||
loop {
|
||||
match read() {
|
||||
Ok(event) => match event {
|
||||
Event::Key(event) => return Ok(event.code),
|
||||
_ => todo!(),
|
||||
},
|
||||
Err(err) => return Err(err),
|
||||
fn draw_row(&self, row: &Row) {
|
||||
let width = self.terminal.size().width as usize;
|
||||
let start = self.offset.x;
|
||||
let end = self.offset.x + width;
|
||||
let row = row.render(start, end);
|
||||
println!("{}\r", row)
|
||||
}
|
||||
|
||||
fn draw_rows(&self) {
|
||||
let height = self.terminal.size().height;
|
||||
for terminal_row in 0..height {
|
||||
Terminal::clear_current_line();
|
||||
if let Some(row) = self.document.row(terminal_row as usize + self.offset.y) {
|
||||
self.draw_row(row);
|
||||
} else if self.document.is_empty() && terminal_row == height / 3 {
|
||||
self.draw_welcome_message()
|
||||
} else {
|
||||
println!("~\r");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default() -> Self {
|
||||
let args: Vec<String> = env::args().collect();
|
||||
let mut initial_status = String::from("HELP: CTRL-Q = quit");
|
||||
let document = if args.len() > 1 {
|
||||
let file_name = &args[1];
|
||||
let doc = Document::open(&file_name);
|
||||
if doc.is_ok() {
|
||||
doc.unwrap()
|
||||
} else {
|
||||
initial_status = format!("ERR: Could not open file: {}", file_name);
|
||||
Document::default()
|
||||
}
|
||||
} else {
|
||||
Document::default()
|
||||
};
|
||||
Self {
|
||||
should_quit: false,
|
||||
terminal: Terminal::default().expect("Failed to initialize terminal"),
|
||||
document,
|
||||
cursor_position: Position::default(),
|
||||
offset: Position::default(),
|
||||
status_message: StatusMessage::from(initial_status),
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_status_bar(&self) {
|
||||
let mut status;
|
||||
let width = self.terminal.size().width as usize;
|
||||
let mut file_name = "[No Name]".to_string();
|
||||
if let Some(name) = &self.document.file_name {
|
||||
file_name = name.clone();
|
||||
file_name.truncate(20);
|
||||
}
|
||||
status = format!("{} - {} lines", file_name, self.document.len());
|
||||
|
||||
let line_indicator = format!(
|
||||
"{}/{}",
|
||||
self.cursor_position.y.saturating_add(1),
|
||||
self.document.len(),
|
||||
);
|
||||
let len = status.len() + line_indicator.len();
|
||||
if width > len {
|
||||
status.push_str(&" ".repeat(width - len));
|
||||
}
|
||||
status = format!("{}{}", status, line_indicator);
|
||||
status.truncate(width);
|
||||
|
||||
Terminal::set_bg_color(STATUS_BG_COLOR);
|
||||
Terminal::set_fg_color(STATUS_FG_COLOR);
|
||||
println!("{}\r", status);
|
||||
Terminal::reset_fg_color();
|
||||
Terminal::reset_bg_color();
|
||||
}
|
||||
|
||||
fn draw_message_bar(&self) {
|
||||
Terminal::clear_current_line();
|
||||
let message = &self.status_message;
|
||||
if Instant::now() - message.time < Duration::new(5, 0) {
|
||||
let mut text = message.text.clone();
|
||||
text.truncate(self.terminal.size().width as usize);
|
||||
print!("{}", text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn die(e: std::io::Error) {
|
||||
Terminal::clear_screen();
|
||||
panic!("{}", e);
|
||||
}
|
||||
|
@ -1,7 +1,14 @@
|
||||
#![warn(clippy::all, clippy::pedantic)]
|
||||
|
||||
mod document;
|
||||
mod editor;
|
||||
mod row;
|
||||
mod terminal;
|
||||
pub use document::Document;
|
||||
use editor::Editor;
|
||||
pub use editor::Position;
|
||||
pub use row::Row;
|
||||
pub use terminal::Terminal;
|
||||
|
||||
fn main() {
|
||||
Editor::default().run();
|
||||
|
91
src/row.rs
Normal file
91
src/row.rs
Normal file
@ -0,0 +1,91 @@
|
||||
use std::cmp;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
use crate::Position;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Row {
|
||||
string: String,
|
||||
len: usize,
|
||||
}
|
||||
|
||||
impl From<&str> for Row {
|
||||
fn from(slice: &str) -> Self {
|
||||
let mut row = Self {
|
||||
string: String::from(slice),
|
||||
len: 0,
|
||||
};
|
||||
row.update_len();
|
||||
row
|
||||
}
|
||||
}
|
||||
|
||||
impl Row {
|
||||
pub fn render(&self, start: usize, end: usize) -> String {
|
||||
let end = cmp::min(end, self.string.len());
|
||||
let start = cmp::min(start, end);
|
||||
let mut result = String::new();
|
||||
for grapheme in self.string[..]
|
||||
.graphemes(true)
|
||||
.skip(start)
|
||||
.take(end - start)
|
||||
{
|
||||
if grapheme == "\t" {
|
||||
result.push_str(" ");
|
||||
} else {
|
||||
result.push_str(grapheme);
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.len
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len == 0
|
||||
}
|
||||
|
||||
pub fn update_len(&mut self) {
|
||||
self.len = self.string[..].graphemes(true).count();
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, at: usize, c: char) {
|
||||
if at >= self.len() {
|
||||
self.string.push(c);
|
||||
} else {
|
||||
let mut result: String = self.string[..].graphemes(true).take(at).collect();
|
||||
let remainder: String = self.string[..].graphemes(true).skip(at).collect();
|
||||
result.push(c);
|
||||
result.push_str(&remainder);
|
||||
self.string = result;
|
||||
}
|
||||
self.update_len();
|
||||
}
|
||||
|
||||
pub fn delete(&mut self, at: usize) {
|
||||
if at >= self.len() {
|
||||
return;
|
||||
} else {
|
||||
let mut result: String = self.string[..].graphemes(true).take(at).collect();
|
||||
let remainder: String = self.string[..].graphemes(true).skip(at + 1).collect();
|
||||
result.push_str(&remainder);
|
||||
self.string = result;
|
||||
}
|
||||
self.update_len();
|
||||
}
|
||||
|
||||
pub fn append(&mut self, new: &Self) {
|
||||
self.string = format!("{}{}", self.string, new.string);
|
||||
self.update_len();
|
||||
}
|
||||
|
||||
pub fn split(&mut self, at: usize) -> Self {
|
||||
let beginning: String = self.string[..].graphemes(true).take(at).collect();
|
||||
let remainder: String = self.string[..].graphemes(true).skip(at).collect();
|
||||
self.string = beginning;
|
||||
self.update_len();
|
||||
Self::from(&remainder[..])
|
||||
}
|
||||
}
|
90
src/terminal.rs
Normal file
90
src/terminal.rs
Normal file
@ -0,0 +1,90 @@
|
||||
use std::io::{self, stdout, Write};
|
||||
|
||||
use termion::{
|
||||
color,
|
||||
event::Key,
|
||||
input::TermRead,
|
||||
raw::{IntoRawMode, RawTerminal},
|
||||
};
|
||||
|
||||
use crate::Position;
|
||||
|
||||
pub struct Size {
|
||||
pub width: u16,
|
||||
pub height: u16,
|
||||
}
|
||||
|
||||
pub struct Terminal {
|
||||
size: Size,
|
||||
_stdout: RawTerminal<std::io::Stdout>,
|
||||
}
|
||||
|
||||
impl Terminal {
|
||||
pub fn default() -> Result<Self, std::io::Error> {
|
||||
let size = termion::terminal_size()?;
|
||||
Ok(Self {
|
||||
size: Size {
|
||||
height: size.1.saturating_sub(2),
|
||||
width: size.0,
|
||||
},
|
||||
_stdout: stdout().into_raw_mode()?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn size(&self) -> &Size {
|
||||
&self.size
|
||||
}
|
||||
|
||||
pub fn clear_screen() {
|
||||
print!("{}", termion::clear::All)
|
||||
}
|
||||
|
||||
pub fn cursor_position(position: &Position) {
|
||||
let Position { mut x, mut y } = position;
|
||||
x = x.saturating_add(1);
|
||||
y = y.saturating_add(1);
|
||||
let x = x as u16;
|
||||
let y = y as u16;
|
||||
print!("{}", termion::cursor::Goto(x, y));
|
||||
}
|
||||
|
||||
pub fn flush() -> Result<(), std::io::Error> {
|
||||
io::stdout().flush()
|
||||
}
|
||||
|
||||
pub fn read_key() -> Result<Key, std::io::Error> {
|
||||
loop {
|
||||
if let Some(key) = io::stdin().lock().keys().next() {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cursor_hide() {
|
||||
print!("{}", termion::cursor::Hide);
|
||||
}
|
||||
|
||||
pub fn cursor_show() {
|
||||
print!("{}", termion::cursor::Show);
|
||||
}
|
||||
|
||||
pub fn clear_current_line() {
|
||||
print!("{}", termion::clear::CurrentLine);
|
||||
}
|
||||
|
||||
pub fn set_bg_color(color: color::Rgb) {
|
||||
print!("{}", color::Bg(color));
|
||||
}
|
||||
|
||||
pub fn reset_bg_color() {
|
||||
print!("{}", color::Bg(color::Reset));
|
||||
}
|
||||
|
||||
pub fn set_fg_color(color: color::Rgb) {
|
||||
print!("{}", color::Fg(color));
|
||||
}
|
||||
|
||||
pub fn reset_fg_color() {
|
||||
print!("{}", color::Fg(color::Reset));
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user