529 lines
17 KiB
Rust
529 lines
17 KiB
Rust
#![allow(dead_code)]
|
|
|
|
use std::cmp::min;
|
|
use std::io;
|
|
|
|
use termion;
|
|
use termion::event::{Event, Key};
|
|
use termion::input::TermRead;
|
|
|
|
use editor::Editor;
|
|
use formatter::{block_index_and_offset, LineFormatter, LINE_BLOCK_LENGTH};
|
|
use self::formatter::ConsoleLineFormatter;
|
|
use string_utils::{is_line_ending, line_ending_to_str, LineEnding};
|
|
use utils::digit_count;
|
|
|
|
pub mod formatter;
|
|
mod screen;
|
|
mod smallstring;
|
|
|
|
use self::screen::{Color, Screen, Style};
|
|
|
|
/// Generalized ui loop.
|
|
macro_rules! ui_loop {
|
|
($term_ui:ident, draw $draw:block, key_press($key:ident) $key_press:block) => {
|
|
let mut stop = false;
|
|
|
|
loop {
|
|
// Draw the editor to screen
|
|
{$draw};
|
|
$term_ui.screen.present();
|
|
|
|
// Handle input
|
|
match $term_ui.inp.next() {
|
|
Some(Ok(Event::Key($key))) => {
|
|
let status = || -> LoopStatus {
|
|
$key_press
|
|
}();
|
|
if status == LoopStatus::Done {
|
|
stop = true;
|
|
// break;
|
|
}
|
|
}
|
|
|
|
_ => {
|
|
// break;
|
|
}
|
|
}
|
|
|
|
// Check for screen resize
|
|
let (w, h) = termion::terminal_size().unwrap();
|
|
if $term_ui.width != w as usize || $term_ui.height != h as usize {
|
|
$term_ui.width = w as usize;
|
|
$term_ui.height = h as usize;
|
|
$term_ui.editor.update_dim($term_ui.height - 1, $term_ui.width);
|
|
$term_ui.screen.resize(w as usize, h as usize);
|
|
}
|
|
|
|
if stop || $term_ui.quit {
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
pub struct TermUI<'a> {
|
|
inp: termion::input::Events<io::StdinLock<'a>>,
|
|
screen: Screen,
|
|
editor: Editor<ConsoleLineFormatter>,
|
|
width: usize,
|
|
height: usize,
|
|
quit: bool,
|
|
}
|
|
|
|
#[derive(Debug, Copy, Clone, PartialEq)]
|
|
enum LoopStatus {
|
|
Done,
|
|
Continue,
|
|
}
|
|
|
|
impl<'a> TermUI<'a> {
|
|
pub fn new<'b>(stdin: &'b mut io::Stdin) -> TermUI<'b> {
|
|
TermUI::new_from_editor(stdin, Editor::new(ConsoleLineFormatter::new(4)))
|
|
}
|
|
|
|
pub fn new_from_editor<'b>(
|
|
stdin: &'b mut io::Stdin,
|
|
ed: Editor<ConsoleLineFormatter>,
|
|
) -> TermUI<'b> {
|
|
let (w, h) = termion::terminal_size().unwrap();
|
|
let mut editor = ed;
|
|
editor.update_dim(h as usize - 1, w as usize);
|
|
|
|
TermUI {
|
|
inp: stdin.lock().events(),
|
|
screen: Screen::new(),
|
|
editor: editor,
|
|
width: w as usize,
|
|
height: h as usize,
|
|
quit: false,
|
|
}
|
|
}
|
|
|
|
pub fn main_ui_loop(&mut self) {
|
|
// Hide cursor
|
|
self.screen.hide_cursor();
|
|
|
|
self.editor.update_dim(self.height - 1, self.width);
|
|
self.editor.formatter.set_wrap_width(self.width as usize);
|
|
|
|
ui_loop!(
|
|
self,
|
|
|
|
// Draw
|
|
draw {
|
|
self.editor.update_view_dim();
|
|
self.editor.formatter.set_wrap_width(self.editor.view_dim.1);
|
|
self.screen.clear();
|
|
self.draw_editor(&self.editor, (0, 0), (self.height - 1, self.width - 1));
|
|
},
|
|
|
|
// Handle input
|
|
key_press(key) {
|
|
match key {
|
|
Key::Ctrl('q') => {
|
|
self.quit = true;
|
|
return LoopStatus::Done;
|
|
}
|
|
|
|
Key::Ctrl('s') => {
|
|
self.editor.save_if_dirty();
|
|
}
|
|
|
|
Key::Ctrl('z') => {
|
|
self.editor.undo();
|
|
}
|
|
|
|
Key::Ctrl('y') => {
|
|
self.editor.redo();
|
|
}
|
|
|
|
Key::Ctrl('l') => {
|
|
self.go_to_line_ui_loop();
|
|
}
|
|
|
|
Key::PageUp => {
|
|
self.editor.page_up();
|
|
}
|
|
|
|
Key::PageDown => {
|
|
self.editor.page_down();
|
|
}
|
|
|
|
Key::Up => {
|
|
self.editor.cursor_up(1);
|
|
}
|
|
|
|
Key::Down => {
|
|
self.editor.cursor_down(1);
|
|
}
|
|
|
|
Key::Left => {
|
|
self.editor.cursor_left(1);
|
|
}
|
|
|
|
Key::Right => {
|
|
self.editor.cursor_right(1);
|
|
}
|
|
|
|
Key::Char('\n') => {
|
|
let nl = line_ending_to_str(self.editor.line_ending_type);
|
|
self.editor.insert_text_at_cursor(nl);
|
|
}
|
|
|
|
Key::Char(' ') => {
|
|
self.editor.insert_text_at_cursor(" ");
|
|
}
|
|
|
|
Key::Char('\t') => {
|
|
self.editor.insert_tab_at_cursor();
|
|
}
|
|
|
|
Key::Backspace => {
|
|
self.editor.backspace_at_cursor();
|
|
}
|
|
|
|
Key::Delete => {
|
|
self.editor.remove_text_in_front_of_cursor(1);
|
|
}
|
|
|
|
// Character
|
|
Key::Char(c) => {
|
|
self.editor.insert_text_at_cursor(&c.to_string()[..]);
|
|
}
|
|
|
|
k => {
|
|
println!("{:?}", k);
|
|
}
|
|
}
|
|
|
|
LoopStatus::Continue
|
|
}
|
|
);
|
|
}
|
|
|
|
fn go_to_line_ui_loop(&mut self) {
|
|
let style = Style(Color::Black, Color::Cyan);
|
|
|
|
let mut cancel = false;
|
|
let prefix = "Jump to line: ";
|
|
let mut line = String::new();
|
|
|
|
ui_loop!(
|
|
self,
|
|
|
|
// Draw
|
|
draw {
|
|
self.editor.update_view_dim();
|
|
self.editor.formatter.set_wrap_width(self.editor.view_dim.1);
|
|
self.screen.clear();
|
|
self.draw_editor(&self.editor, (0, 0), (self.height - 1, self.width - 1));
|
|
for i in 0..self.width {
|
|
self.screen.draw(i, 0, " ", style);
|
|
}
|
|
self.screen.draw(1, 0, prefix, style);
|
|
self.screen.draw(
|
|
prefix.len() + 1,
|
|
0,
|
|
&line[..],
|
|
style,
|
|
);
|
|
},
|
|
|
|
// Handle input
|
|
key_press(key) {
|
|
match key {
|
|
Key::Esc => {
|
|
cancel = true;
|
|
return LoopStatus::Done;
|
|
}
|
|
|
|
Key::Char('\n') => {
|
|
return LoopStatus::Done;
|
|
}
|
|
|
|
Key::Backspace => {
|
|
line.pop();
|
|
}
|
|
|
|
// Character
|
|
Key::Char(c) => {
|
|
if c.is_numeric() {
|
|
line.push(c);
|
|
}
|
|
}
|
|
|
|
_ => {}
|
|
}
|
|
|
|
return LoopStatus::Continue;
|
|
}
|
|
);
|
|
|
|
// Jump to line!
|
|
if !cancel {
|
|
if let Ok(n) = line.parse() {
|
|
let n2: usize = n; // Weird work-around: the type of n wasn't being inferred
|
|
if n2 > 0 {
|
|
self.editor.jump_to_line(n2 - 1);
|
|
} else {
|
|
self.editor.jump_to_line(0);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn draw_editor(
|
|
&self,
|
|
editor: &Editor<ConsoleLineFormatter>,
|
|
c1: (usize, usize),
|
|
c2: (usize, usize),
|
|
) {
|
|
let style = Style(Color::Black, Color::Cyan);
|
|
|
|
// Fill in top row with info line color
|
|
for i in c1.1..(c2.1 + 1) {
|
|
self.screen.draw(i, c1.0, " ", style);
|
|
}
|
|
|
|
// Filename and dirty marker
|
|
let filename = editor.file_path.display();
|
|
let dirty_char = if editor.dirty { "*" } else { "" };
|
|
let name = format!("{}{}", filename, dirty_char);
|
|
self.screen.draw(c1.1 + 1, c1.0, &name[..], style);
|
|
|
|
// Percentage position in document
|
|
// TODO: use view instead of cursor for calculation if there is more
|
|
// than one cursor.
|
|
let percentage: usize = if editor.buffer.char_count() > 0 {
|
|
(((editor.cursors[0].range.0 as f32) / (editor.buffer.char_count() as f32)) * 100.0)
|
|
as usize
|
|
} else {
|
|
100
|
|
};
|
|
let pstring = format!("{}%", percentage);
|
|
self.screen
|
|
.draw(c2.1 - pstring.len(), c1.0, &pstring[..], style);
|
|
|
|
// Text encoding info and tab style
|
|
let nl = match editor.line_ending_type {
|
|
LineEnding::None => "None",
|
|
LineEnding::CRLF => "CRLF",
|
|
LineEnding::LF => "LF",
|
|
LineEnding::VT => "VT",
|
|
LineEnding::FF => "FF",
|
|
LineEnding::CR => "CR",
|
|
LineEnding::NEL => "NEL",
|
|
LineEnding::LS => "LS",
|
|
LineEnding::PS => "PS",
|
|
};
|
|
let soft_tabs_str = if editor.soft_tabs { "spaces" } else { "tabs" };
|
|
let info_line = format!(
|
|
"UTF8:{} {}:{}",
|
|
nl, soft_tabs_str, editor.soft_tab_width as usize
|
|
);
|
|
self.screen.draw(c2.1 - 30, c1.0, &info_line[..], style);
|
|
|
|
// Draw main text editing area
|
|
self.draw_editor_text(editor, (c1.0 + 1, c1.1), c2);
|
|
}
|
|
|
|
fn draw_editor_text(
|
|
&self,
|
|
editor: &Editor<ConsoleLineFormatter>,
|
|
c1: (usize, usize),
|
|
c2: (usize, usize),
|
|
) {
|
|
// Calculate all the starting info
|
|
let gutter_width = editor.editor_dim.1 - editor.view_dim.1;
|
|
let (line_index, col_i) = editor.buffer.index_to_line_col(editor.view_pos.0);
|
|
let (mut line_block_index, _) = block_index_and_offset(col_i);
|
|
let mut char_index = editor
|
|
.buffer
|
|
.line_col_to_index((line_index, line_block_index * LINE_BLOCK_LENGTH));
|
|
let temp_line = editor.buffer.get_line(line_index);
|
|
let (vis_line_offset, _) = editor.formatter.index_to_v2d(
|
|
temp_line
|
|
.slice(
|
|
line_block_index * LINE_BLOCK_LENGTH,
|
|
min(
|
|
temp_line.len_chars(),
|
|
(line_block_index + 1) * LINE_BLOCK_LENGTH,
|
|
),
|
|
)
|
|
.graphemes(),
|
|
editor.view_pos.0 - char_index,
|
|
);
|
|
|
|
let mut screen_line = c1.0 as isize - vis_line_offset as isize;
|
|
let screen_col = c1.1 as isize + gutter_width as isize;
|
|
|
|
// Fill in the gutter with the appropriate background
|
|
for y in c1.0..(c2.0 + 1) {
|
|
for x in c1.1..(c1.1 + gutter_width - 1) {
|
|
self.screen
|
|
.draw(x, y, " ", Style(Color::White, Color::Blue));
|
|
}
|
|
}
|
|
|
|
let mut line_num = line_index + 1;
|
|
for line in editor.buffer.line_iter_at_index(line_index) {
|
|
// Print line number
|
|
if line_block_index == 0 {
|
|
let lnx = c1.1 + (gutter_width - 1 - digit_count(line_num as u32, 10) as usize);
|
|
let lny = screen_line as usize;
|
|
if lny >= c1.0 && lny <= c2.0 {
|
|
self.screen.draw(
|
|
lnx,
|
|
lny,
|
|
&format!("{}", line_num)[..],
|
|
Style(Color::White, Color::Blue),
|
|
);
|
|
}
|
|
}
|
|
|
|
// Loop through the graphemes of the line and print them to
|
|
// the screen.
|
|
let mut line_g_index: usize = 0;
|
|
let mut last_pos_y = 0;
|
|
let mut lines_traversed: usize = 0;
|
|
let line_len = line.len_chars();
|
|
let mut g_iter = editor.formatter.iter(line.slice(
|
|
line_block_index * LINE_BLOCK_LENGTH,
|
|
line_len,
|
|
).graphemes());
|
|
|
|
loop {
|
|
if let Some((g, (pos_y, pos_x), width)) = g_iter.next() {
|
|
if last_pos_y != pos_y {
|
|
if last_pos_y < pos_y {
|
|
lines_traversed += pos_y - last_pos_y;
|
|
}
|
|
last_pos_y = pos_y;
|
|
}
|
|
// Calculate the cell coordinates at which to draw the grapheme
|
|
let px = pos_x as isize + screen_col - editor.view_pos.1 as isize;
|
|
let py = lines_traversed as isize + screen_line;
|
|
|
|
// If we're off the bottom, we're done
|
|
if py > c2.0 as isize {
|
|
return;
|
|
}
|
|
|
|
// Draw the grapheme to the screen if it's in bounds
|
|
if (px >= c1.1 as isize) && (py >= c1.0 as isize) && (px <= c2.1 as isize) {
|
|
// Check if the character is within a cursor
|
|
let mut at_cursor = false;
|
|
for c in editor.cursors.iter() {
|
|
if char_index >= c.range.0 && char_index <= c.range.1 {
|
|
at_cursor = true;
|
|
}
|
|
}
|
|
|
|
// Actually print the character
|
|
if is_line_ending(g) {
|
|
if at_cursor {
|
|
self.screen.draw(
|
|
px as usize,
|
|
py as usize,
|
|
" ",
|
|
Style(Color::Black, Color::White),
|
|
);
|
|
}
|
|
} else if g == "\t" {
|
|
for i in 0..width {
|
|
let tpx = px as usize + i;
|
|
if tpx <= c2.1 {
|
|
self.screen.draw(
|
|
tpx as usize,
|
|
py as usize,
|
|
" ",
|
|
Style(Color::White, Color::Black),
|
|
);
|
|
}
|
|
}
|
|
|
|
if at_cursor {
|
|
self.screen.draw(
|
|
px as usize,
|
|
py as usize,
|
|
" ",
|
|
Style(Color::Black, Color::White),
|
|
);
|
|
}
|
|
} else {
|
|
if at_cursor {
|
|
self.screen.draw(
|
|
px as usize,
|
|
py as usize,
|
|
g,
|
|
Style(Color::Black, Color::White),
|
|
);
|
|
} else {
|
|
self.screen.draw(
|
|
px as usize,
|
|
py as usize,
|
|
g,
|
|
Style(Color::White, Color::Black),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
char_index += g.chars().count();
|
|
line_g_index += 1;
|
|
} else {
|
|
break;
|
|
}
|
|
|
|
if line_g_index >= LINE_BLOCK_LENGTH {
|
|
line_block_index += 1;
|
|
line_g_index = 0;
|
|
let line_len = line.len_chars();
|
|
g_iter = editor.formatter.iter(line.slice(
|
|
line_block_index * LINE_BLOCK_LENGTH,
|
|
line_len,
|
|
).graphemes());
|
|
lines_traversed += 1;
|
|
}
|
|
}
|
|
|
|
line_block_index = 0;
|
|
screen_line += lines_traversed as isize + 1;
|
|
line_num += 1;
|
|
}
|
|
|
|
// If we get here, it means we reached the end of the text buffer
|
|
// without going off the bottom of the screen. So draw the cursor
|
|
// at the end if needed.
|
|
|
|
// Check if the character is within a cursor
|
|
let mut at_cursor = false;
|
|
for c in editor.cursors.iter() {
|
|
if char_index >= c.range.0 && char_index <= c.range.1 {
|
|
at_cursor = true;
|
|
}
|
|
}
|
|
|
|
if at_cursor {
|
|
// Calculate the cell coordinates at which to draw the cursor
|
|
let pos_x = editor
|
|
.formatter
|
|
.index_to_horizontal_v2d(&self.editor.buffer, self.editor.buffer.char_count());
|
|
let px = pos_x as isize + screen_col - editor.view_pos.1 as isize;
|
|
let py = screen_line - 1;
|
|
|
|
if (px >= c1.1 as isize) && (py >= c1.0 as isize) && (px <= c2.1 as isize)
|
|
&& (py <= c2.0 as isize)
|
|
{
|
|
self.screen.draw(
|
|
px as usize,
|
|
py as usize,
|
|
" ",
|
|
Style(Color::Black, Color::White),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|