use crate::commands::ALL_COMMANDS;
use crate::{
commands, utils, AnsiPosition, Boundary, Config, Console, Document, Help, History, LineNumber,
Mode, Navigator, OperationType, Row, RowIndex,
};
use serde::ser::{SerializeStruct, Serializer};
use serde::Serialize;
use std::cmp;
use std::io;
use std::path::PathBuf;
use termion::color;
use termion::event::{Event, Key, MouseButton, MouseEvent};
use unicode_segmentation::UnicodeSegmentation;
const STATUS_FG_COLOR: color::Rgb = color::Rgb(63, 63, 63);
const STATUS_BG_COLOR: color::Rgb = color::Rgb(239, 239, 239);
const PKG: &str = "bo";
const COMMAND_PREFIX: char = ':';
const SEARCH_PREFIX: char = '/';
const AUTOCOMPLETION_SUGGESTIONS_SEPARATOR: char = '|';
const LINE_NUMBER_OFFSET: u8 = 4; const START_X: u8 = LINE_NUMBER_OFFSET as u8; const SPACES_PER_TAB: usize = 4;
const SWAP_SAVE_EVERY: u8 = 100;
#[derive(Debug, Default, PartialEq, Clone, Copy, Serialize)]
pub struct Position {
pub x: usize,
pub y: usize,
}
impl Position {
pub fn reset_x(&mut self) {
self.x = 0;
}
#[must_use]
pub fn top_left() -> Self {
Self::default()
}
}
impl From<AnsiPosition> for Position {
fn from(p: AnsiPosition) -> Self {
Self {
x: p.x.saturating_sub(1) as usize,
y: p.y.saturating_sub(1) as usize,
}
}
}
#[derive(Debug, Default, Serialize)]
pub struct ViewportOffset {
pub rows: usize,
pub columns: usize,
}
#[derive(Debug)]
enum Direction {
Up,
Down,
Left,
Right,
}
#[derive(Debug)]
pub struct Editor {
should_quit: bool,
cursor_position: Position,
document: Document,
offset: ViewportOffset,
message: String,
mode: Mode,
command_buffer: String,
command_suggestions: Vec<String>,
current_autocompletion_index: usize,
config: Config,
normal_command_buffer: Vec<String>,
mouse_event_buffer: Vec<Position>,
search_matches: Vec<(Position, Position)>,
current_search_match_index: usize,
alternate_screen: bool,
last_saved_hash: u64,
terminal: Box<dyn Console>,
unsaved_edits: u8,
row_prefix_length: u8,
help_message: String,
history: History,
}
fn die(e: &io::Error) {
print!("{}", termion::clear::All);
panic!("{}", e);
}
impl Serialize for Editor {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut s = serializer.serialize_struct("Editor", 10)?;
s.serialize_field("cursor_position", &self.cursor_position)?;
s.serialize_field("offset", &self.offset)?;
s.serialize_field("mode", format!("{}", self.mode).as_str())?;
s.serialize_field("command_buffer", &self.command_buffer)?;
s.serialize_field("normal_command_buffer", &self.normal_command_buffer)?;
s.serialize_field("search_matches", &self.search_matches)?;
s.serialize_field(
"current_search_match_index",
&self.current_search_match_index,
)?;
s.serialize_field("unsaved_edits", &self.unsaved_edits)?;
s.serialize_field("last_saved_hash", &self.last_saved_hash)?;
s.serialize_field("row_prefix_length", &self.row_prefix_length)?;
s.serialize_field("document", &self.document)?;
s.serialize_field("command_suggestions", &self.command_suggestions)?;
s.serialize_field(
"current_autocompletion_index",
&self.current_autocompletion_index,
)?;
s.end()
}
}
impl Editor {
pub fn new(filename: Option<String>, terminal: Box<dyn Console>) -> Self {
let document: Document = match filename {
None => Document::default(),
Some(path) => Document::open(std::path::PathBuf::from(utils::expand_tilde(&path)))
.unwrap_or_default(),
};
let last_saved_hash = document.hashed();
let help_message = Help::default().format();
Self {
should_quit: false,
cursor_position: Position::top_left(),
document,
offset: ViewportOffset::default(),
message: "".to_string(),
mode: Mode::Normal,
command_buffer: "".to_string(),
command_suggestions: vec![],
current_autocompletion_index: 0,
config: Config::default(),
normal_command_buffer: vec![],
mouse_event_buffer: vec![],
search_matches: vec![],
current_search_match_index: 0,
alternate_screen: false,
terminal,
unsaved_edits: 0,
last_saved_hash,
row_prefix_length: 0,
help_message,
history: History::default(),
}
}
pub fn run(&mut self) {
loop {
if let Err(error) = self.refresh_screen() {
die(&error);
}
if let Err(error) = self.process_event() {
die(&error);
}
if self.should_quit {
self.terminal.clear_screen();
break;
}
}
}
fn process_event(&mut self) -> Result<(), std::io::Error> {
let event = self.terminal.read_event()?;
match event {
Event::Key(pressed_key) => self.process_keystroke(pressed_key),
Event::Mouse(mouse_event) => self.process_mouse_event(mouse_event),
Event::Unsupported(_) => (),
}
Ok(())
}
fn process_keystroke(&mut self, pressed_key: Key) {
if self.is_receiving_command() {
if self.is_autocompleting_command() {
match pressed_key {
Key::Char('\t') => self.cycle_through_command_suggestions(),
Key::Char('\n') => {
self.command_buffer = format!(
"{}{}",
COMMAND_PREFIX,
self.command_suggestions[self.current_autocompletion_index].clone()
);
self.reset_autocompletions();
self.process_keystroke(pressed_key);
}
_ => {
self.reset_autocompletions();
self.process_keystroke(pressed_key);
}
}
} else {
match pressed_key {
Key::Esc => self.stop_receiving_command(),
Key::Char('\n') => {
self.process_received_command();
self.stop_receiving_command();
}
Key::Char('\t') => self.autocomplete_command(),
Key::Char(c) => self.command_buffer.push(c), Key::Backspace => self
.command_buffer
.truncate(self.command_buffer.len().saturating_sub(1)),
_ => (),
}
}
} else {
match self.mode {
Mode::Normal => self.process_normal_command(pressed_key),
Mode::Insert => self.process_insert_command(pressed_key),
}
}
}
fn process_mouse_event(&mut self, mouse_event: MouseEvent) {
match mouse_event {
MouseEvent::Press(MouseButton::Left, _, _) => self.mouse_event_buffer.push(
self.terminal
.get_cursor_index_from_mouse_event(mouse_event, self.row_prefix_length),
),
MouseEvent::Release(_, _) => {
if !self.mouse_event_buffer.is_empty() {
let cursor_position = self.mouse_event_buffer.pop().unwrap();
if cursor_position.y.saturating_add(1) <= self.document.num_rows() {
if let Some(target_row) = self.get_row(RowIndex::new(cursor_position.y)) {
if cursor_position.x <= target_row.len() {
self.cursor_position = cursor_position;
}
}
}
}
}
_ => (),
}
}
fn enter_insert_mode(&mut self) {
self.mode = Mode::Insert;
self.terminal.set_cursor_as_steady_bar();
}
fn enter_normal_mode(&mut self) {
self.mode = Mode::Normal;
self.terminal.set_cursor_as_steady_block();
}
fn start_receiving_command(&mut self) {
self.command_buffer.push(COMMAND_PREFIX);
}
fn start_receiving_search_pattern(&mut self) {
self.command_buffer.push(SEARCH_PREFIX);
}
fn stop_receiving_command(&mut self) {
self.command_buffer = "".to_string();
}
fn is_receiving_command(&self) -> bool {
!self.command_buffer.is_empty()
}
fn is_autocompleting_command(&self) -> bool {
self.is_receiving_command() && !self.command_suggestions.is_empty()
}
fn reset_autocompletions(&mut self) {
self.command_suggestions = vec![];
self.current_autocompletion_index = 0;
}
fn pop_normal_command_repetitions(&mut self) -> usize {
let times = match self.normal_command_buffer.len() {
0 => 1,
_ => self
.normal_command_buffer
.join("")
.parse::<usize>()
.unwrap(),
};
self.normal_command_buffer = vec![];
times
}
fn process_received_command(&mut self) {
let command = self.command_buffer.clone();
match self.command_buffer.chars().next().unwrap() {
SEARCH_PREFIX => {
self.process_search_command(command.strip_prefix(SEARCH_PREFIX).unwrap());
}
COMMAND_PREFIX => {
let command = command.strip_prefix(COMMAND_PREFIX).unwrap_or_default();
if command.is_empty() {
} else if command.chars().all(char::is_numeric) {
let line_number = command.parse::<usize>().unwrap();
self.goto_line(LineNumber::new(line_number), 0);
} else if command.split(' ').count() > 1 {
let cmd_tokens: Vec<&str> = command.split(' ').collect();
match *cmd_tokens.get(0).unwrap_or(&"") {
commands::OPEN | commands::OPEN_SHORT => {
let filename = PathBuf::from(cmd_tokens[1]);
if self.document.filename == Some(filename.clone()) {
self.display_message(format!(
"{} is already opened",
cmd_tokens[1]
));
} else if let Ok(document) = Document::open(filename) {
self.document = document;
self.last_saved_hash = self.document.hashed();
self.reset_message();
self.cursor_position = Position::default();
self.history = History::default();
} else {
self.display_message(utils::red(&format!(
"{} not found",
cmd_tokens[1]
)));
}
}
commands::NEW => {
self.document =
Document::new_empty(PathBuf::from(cmd_tokens[1].to_string()));
self.enter_insert_mode();
}
commands::SAVE => {
let new_name = cmd_tokens[1..].join(" ");
self.save(new_name.trim());
}
_ => self.display_message(utils::red(&format!(
"Unknown command '{}'",
cmd_tokens[0]
))),
}
} else {
match command {
commands::FORCE_QUIT => self.quit(true),
commands::QUIT => self.quit(false),
commands::LINE_NUMBERS => {
self.config.display_line_numbers =
Config::toggle(self.config.display_line_numbers);
self.row_prefix_length = if self.config.display_line_numbers {
START_X
} else {
0
};
}
commands::STATS => {
self.config.display_stats = Config::toggle(self.config.display_stats);
}
commands::HELP => {
self.alternate_screen = true;
}
commands::SAVE => self.save(""),
commands::SAVE_AND_QUIT => {
self.save("");
self.quit(false);
}
commands::DEBUG => {
if let Ok(state) = serde_json::to_string_pretty(&self) {
utils::log(state.as_str());
}
}
_ => self
.display_message(utils::red(&format!("Unknown command '{}'", command))),
}
}
}
_ => (),
}
}
fn autocomplete_command(&mut self) {
let mut matches: Vec<String> = vec![];
let current_command = self
.command_buffer
.strip_prefix(COMMAND_PREFIX)
.unwrap_or_default();
for command_str in ALL_COMMANDS {
if command_str.starts_with(¤t_command) {
matches.push(command_str.to_owned());
}
}
match matches.len() {
0 => (),
1 => self.command_buffer = format!("{}{}", COMMAND_PREFIX, matches[0]),
_ => self.command_suggestions = matches,
}
}
fn cycle_through_command_suggestions(&mut self) {
let next_suggestion_index = if self.current_autocompletion_index
== self.command_suggestions.len().saturating_sub(1)
{
0
} else {
self.current_autocompletion_index.saturating_add(1)
};
self.current_autocompletion_index = next_suggestion_index;
}
fn save(&mut self, new_name: &str) {
self.document.trim_trailing_spaces();
if self.cursor_position.x >= self.current_row().len() {
self.cursor_position.x = self.current_row().len().saturating_sub(1);
}
let initial_filename = self.document.filename.clone();
if new_name.is_empty() {
if self.document.filename.is_none() {
self.display_message(utils::red("No file name"));
return;
} else if self.document.save().is_ok() {
self.display_message("File successfully saved".to_string());
self.last_saved_hash = self.document.hashed();
} else {
self.display_message(utils::red("Error writing to file!"));
return;
}
} else if self.document.save_as(new_name).is_ok() {
if initial_filename.is_none() {
self.display_message(format!("Buffer saved to {}", new_name));
} else {
self.display_message(format!(
"{} successfully renamed to {}",
self.document
.filename
.as_ref()
.unwrap()
.to_str()
.unwrap_or_default(),
new_name
));
}
self.document.filename = Some(PathBuf::from(new_name));
} else {
self.display_message(utils::red("Error writing to file!"));
}
self.unsaved_edits = 0;
self.last_saved_hash = self.document.hashed();
}
fn save_to_swap_file(&mut self) {
if self.document.save_to_swap_file().is_ok() {
self.unsaved_edits = 0;
}
}
fn quit(&mut self, force: bool) {
if self.is_dirty() && !force {
self.display_message(utils::red("Unsaved changes! Run :q! to override"));
} else {
self.should_quit = true;
}
}
fn process_search_command(&mut self, search_pattern: &str) {
self.reset_search();
for (row_index, row) in self.document.iter().enumerate() {
if row.contains(search_pattern) {
for match_start_index in row.find_all(search_pattern) {
let match_start = Position {
x: match_start_index,
y: row_index.saturating_add(1), };
let match_end = Position {
x: match_start_index
.saturating_add(1)
.saturating_add(search_pattern.len()),
y: row_index.saturating_add(1),
};
self.search_matches.push((match_start, match_end));
}
}
}
self.display_message(format!("{} matches", self.search_matches.len()));
self.current_search_match_index = self.search_matches.len().saturating_sub(1);
self.goto_next_search_match();
}
fn reset_search(&mut self) {
self.search_matches = vec![]; self.current_search_match_index = 0;
}
fn revert_to_main_screen(&mut self) {
self.reset_message();
self.alternate_screen = false;
}
fn process_normal_command(&mut self, key: Key) {
if key == Key::Esc {
self.reset_message();
self.reset_search();
}
if let Key::Char(c) = key {
match c {
'0' => {
if self.normal_command_buffer.is_empty() {
self.goto_start_or_end_of_line(&Boundary::Start);
} else {
self.normal_command_buffer.push(c.to_string());
}
}
'1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' => {
self.normal_command_buffer.push(c.to_string());
}
'i' => self.enter_insert_mode(),
':' => self.start_receiving_command(),
'/' => self.start_receiving_search_pattern(),
'G' => self.goto_start_or_end_of_document(&Boundary::End),
'g' => self.goto_start_or_end_of_document(&Boundary::Start),
'$' => self.goto_start_or_end_of_line(&Boundary::End),
'^' => self.goto_first_non_whitespace(),
'H' => self.goto_first_line_of_terminal(),
'M' => self.goto_middle_of_terminal(),
'L' => self.goto_last_line_of_terminal(),
'm' => self.goto_matching_closing_symbol(),
'n' => self.goto_next_search_match(),
'N' => self.goto_previous_search_match(),
'q' => self.revert_to_main_screen(),
'd' => self.delete_current_line(),
'x' => self.delete_current_grapheme(),
'o' => self.insert_newline_after_current_line(),
'O' => self.insert_newline_before_current_line(),
'A' => self.append_to_line(),
'J' => self.join_current_line_with_next_one(),
'u' => self.undo_last_operation(),
_ => {
let times = self.pop_normal_command_repetitions();
self.process_normal_command_n_times(c, times);
}
}
};
}
fn process_normal_command_n_times(&mut self, c: char, n: usize) {
match c {
'b' => self.goto_start_or_end_of_word(&Boundary::Start, n),
'w' => self.goto_start_or_end_of_word(&Boundary::End, n),
'h' => self.move_cursor(&Direction::Left, n),
'j' => self.move_cursor(&Direction::Down, n),
'k' => self.move_cursor(&Direction::Up, n),
'l' => self.move_cursor(&Direction::Right, n),
'}' => self.goto_start_or_end_of_paragraph(&Boundary::End, n),
'{' => self.goto_start_or_end_of_paragraph(&Boundary::Start, n),
'%' => self.goto_percentage_in_document(n),
_ => (),
}
}
fn process_insert_command(&mut self, pressed_key: Key) {
match pressed_key {
Key::Esc => {
self.enter_normal_mode();
return;
}
Key::Backspace => {
if self.cursor_position.x == 0 {
if self.cursor_position.y > 0 {
let previous_line_len =
self.get_row(self.previous_row_index()).unwrap().len();
self.history.register_deletion("\n", self.cursor_position);
self.document.delete(0, 0, self.current_row_index());
self.goto_x_y(previous_line_len, self.previous_row_index());
}
} else {
let previous_grapheme = self.previous_grapheme().to_string();
self.history
.register_deletion(previous_grapheme.as_str(), self.cursor_position);
self.document.delete(
self.current_x_position().saturating_sub(1),
self.current_x_position(),
self.current_row_index(),
);
self.move_cursor(&Direction::Left, 1);
}
}
Key::Char('\n') => {
self.history.register_insertion("\n", self.cursor_position);
self.document
.insert_newline(self.current_x_position(), self.current_row_index());
self.goto_x_y(0, self.next_row_index());
}
Key::Char('\t') => {
for _ in 0..SPACES_PER_TAB {
self.history.register_insertion(" ", self.cursor_position);
self.document
.insert(' ', self.current_x_position(), self.current_row_index());
self.move_cursor(&Direction::Right, 1);
}
}
Key::Char(c) => {
self.history
.register_insertion(c.to_string().as_str(), self.cursor_position);
self.document
.insert(c, self.current_x_position(), self.current_row_index());
self.move_cursor(&Direction::Right, 1);
}
_ => (),
}
self.unsaved_edits = self.unsaved_edits.saturating_add(1);
if self.unsaved_edits >= SWAP_SAVE_EVERY {
self.save_to_swap_file();
}
}
fn get_row(&self, index: RowIndex) -> Option<&Row> {
self.document.get_row(index)
}
fn current_row(&self) -> &Row {
self.get_row(self.current_row_index()).unwrap()
}
fn current_row_index(&self) -> RowIndex {
RowIndex::new(self.cursor_position.y.saturating_add(self.offset.rows))
}
fn previous_row_index(&self) -> RowIndex {
self.current_row_index().previous()
}
fn next_row_index(&self) -> RowIndex {
self.current_row_index().next()
}
fn current_x_position(&self) -> usize {
self.cursor_position.x.saturating_add(self.offset.columns)
}
fn current_grapheme(&self) -> &str {
self.current_row().nth_grapheme(self.current_x_position())
}
fn previous_grapheme(&self) -> &str {
self.current_row()
.nth_grapheme(self.current_x_position().saturating_sub(1))
}
fn current_line_number(&self) -> LineNumber {
LineNumber::from(self.current_row_index())
}
fn delete_current_line(&mut self) {
let current_row_str = format!("{}\n", self.current_row().reversed());
self.history.register_deletion(
¤t_row_str,
Position {
x: 0,
y: self.cursor_position.y,
},
);
self.document.delete_row(self.current_row_index());
if self.cursor_position.y > self.document.num_rows().saturating_sub(1) {
self.goto_line(
LineNumber::new(self.document.num_rows()),
cmp::min(
self.cursor_position.x,
self.document
.get_row(RowIndex::new(self.cursor_position.y.saturating_sub(1)))
.unwrap_or(&Row::from(""))
.string
.graphemes(true)
.count(),
),
);
} else {
self.cursor_position.reset_x();
}
}
fn delete_current_grapheme(&mut self) {
let current_grapheme = self.current_grapheme().to_string();
self.history
.register_deletion(¤t_grapheme, self.cursor_position);
self.document.delete(
self.current_x_position(),
self.current_x_position(),
self.current_row_index(),
);
}
fn insert_newline_after_current_line(&mut self) {
let eol_position = Position {
x: self.current_row().len(),
y: self.current_row_index().value,
};
self.document
.insert_newline(eol_position.x, RowIndex::new(eol_position.y));
self.history.register_insertion("\n", eol_position);
self.goto_x_y(0, self.next_row_index());
self.enter_insert_mode();
}
fn insert_newline_before_current_line(&mut self) {
let sol_position = Position {
x: 0,
y: self.current_row_index().value,
};
self.document
.insert_newline(sol_position.x, RowIndex::new(sol_position.y));
self.history.register_insertion("\n", sol_position);
self.goto_x_y(0, self.current_row_index());
self.enter_insert_mode();
}
fn append_to_line(&mut self) {
self.enter_insert_mode();
self.goto_start_or_end_of_line(&Boundary::End);
self.move_cursor(&Direction::Right, 1);
}
fn join_current_line_with_next_one(&mut self) {
if self.current_row_index().value < self.document.num_rows() {
self.document.join_row_with_previous_one(
self.current_row().len().saturating_sub(1), self.next_row_index(),
Some(' '),
);
self.goto_start_or_end_of_line(&Boundary::End);
}
}
fn goto_start_or_end_of_paragraph(&mut self, boundary: &Boundary, times: usize) {
for _ in 0..times {
let next_line_number = Navigator::find_line_number_of_start_or_end_of_paragraph(
&self.document,
self.current_line_number(),
boundary,
);
self.goto_line(next_line_number, 0);
}
}
fn goto_start_or_end_of_document(&mut self, boundary: &Boundary) {
match boundary {
Boundary::Start => self.goto_line(LineNumber::new(1), 0),
Boundary::End => self.goto_line(self.document.last_line_number(), 0),
}
}
fn goto_start_or_end_of_line(&mut self, boundary: &Boundary) {
match boundary {
Boundary::Start => self.move_cursor_to_position_x(0),
Boundary::End => {
self.move_cursor_to_position_x(self.current_row().len().saturating_sub(1));
}
}
}
fn goto_start_or_end_of_word(&mut self, boundary: &Boundary, times: usize) {
for _ in 0..times {
let x = Navigator::find_index_of_next_or_previous_word(
self.current_row(),
self.current_x_position(),
boundary,
);
self.move_cursor_to_position_x(x);
}
}
fn goto_first_non_whitespace(&mut self) {
if let Some(x) = Navigator::find_index_of_first_non_whitespace(self.current_row()) {
self.move_cursor_to_position_x(x);
}
}
fn goto_middle_of_terminal(&mut self) {
self.goto_line(
self.terminal
.middle_of_screen_line_number()
.add(self.offset.rows),
0,
);
}
fn goto_first_line_of_terminal(&mut self) {
self.goto_line(LineNumber::new(self.offset.rows.saturating_add(1)), 0);
}
fn goto_last_line_of_terminal(&mut self) {
self.goto_line(
self.terminal
.bottom_of_screen_line_number()
.add(self.offset.rows),
0,
);
}
fn goto_percentage_in_document(&mut self, percent: usize) {
let percent = cmp::min(percent, 100);
let line_number = LineNumber::new(self.document.last_line_number().value * percent / 100);
self.goto_line(line_number, 0);
}
fn goto_matching_closing_symbol(&mut self) {
let current_grapheme = self.current_grapheme();
match current_grapheme {
"\"" | "'" | "{" | "<" | "(" | "[" => {
if let Some(position) = Navigator::find_matching_closing_symbol(
&self.document,
&self.cursor_position,
&self.offset,
) {
self.goto_position(position);
}
}
"}" | ">" | ")" | "]" => {
if let Some(position) = Navigator::find_matching_opening_symbol(
&self.document,
&self.cursor_position,
&self.offset,
) {
self.goto_position(position);
}
}
_ => (),
};
}
fn goto_next_search_match(&mut self) {
if self.search_matches.is_empty() {
return;
}
if self.current_search_match_index == self.search_matches.len().saturating_sub(1) {
self.current_search_match_index = 0;
} else {
self.current_search_match_index = self.current_search_match_index.saturating_add(1);
}
self.display_message(format!(
"Match {}/{}",
self.current_search_match_index.saturating_add(1),
self.search_matches.len()
));
if let Some(search_match) = self.search_matches.get(self.current_search_match_index) {
let x_position = search_match.0.x;
let line_number = LineNumber::new(search_match.0.y);
self.goto_line(line_number, x_position);
}
}
fn goto_previous_search_match(&mut self) {
if self.search_matches.is_empty() {
return;
}
if self.current_search_match_index == 0 {
self.current_search_match_index = self.search_matches.len().saturating_sub(1);
} else {
self.current_search_match_index = self.current_search_match_index.saturating_sub(1);
}
self.display_message(format!(
"Match {}/{}",
self.current_search_match_index.saturating_add(1),
self.search_matches.len()
));
if let Some(search_match) = self.search_matches.get(self.current_search_match_index) {
let line_number = LineNumber::new(search_match.0.y);
let x_position = search_match.0.x;
self.goto_line(line_number, x_position);
}
}
fn goto_line(&mut self, line_number: LineNumber, x_position: usize) {
self.goto_x_y(x_position, RowIndex::from(line_number));
}
fn goto_position(&mut self, pos: Position) {
self.goto_x_y(pos.x, RowIndex::new(pos.y));
}
fn goto_x_y(&mut self, x: usize, y: RowIndex) {
self.move_cursor_to_position_x(x);
self.move_cursor_to_position_y(y);
}
fn move_cursor(&mut self, direction: &Direction, times: usize) {
let size = self.terminal.size();
let term_height = size.height.saturating_sub(1) as usize;
let term_width = size.width.saturating_sub(1) as usize;
let Position { mut x, mut y } = self.cursor_position;
let ViewportOffset {
columns: mut offset_x,
rows: mut offset_y,
} = self.offset;
for _ in 0..times {
match direction {
Direction::Up => {
if y == 0 {
offset_y = offset_y.saturating_sub(1);
} else {
y = y.saturating_sub(1);
}
} Direction::Down => {
if y.saturating_add(offset_y) < self.document.last_line_number().sub(1).value {
if y < term_height {
y = y.saturating_add(1);
} else {
offset_y = offset_y.saturating_add(1);
}
}
}
Direction::Left => {
if x >= term_width {
offset_x = offset_x.saturating_sub(1);
} else {
x = x.saturating_sub(1);
}
}
Direction::Right => {
if x.saturating_add(offset_x) <= self.current_row().len().saturating_sub(1) {
if x < term_width {
x = x.saturating_add(1);
} else {
offset_x = offset_x.saturating_add(1);
}
}
}
}
}
self.cursor_position.y = y;
self.offset.columns = offset_x;
self.offset.rows = offset_y;
if self.mode == Mode::Normal {
self.cursor_position.x = cmp::min(self.current_row().len().saturating_sub(1), x);
} else {
self.cursor_position.x = x;
}
}
fn move_cursor_to_position_y(&mut self, y: RowIndex) {
let max_line_number = self.document.last_line_number(); let term_height = self.terminal.bottom_of_screen_line_number().value;
let middle_of_screen_line_number = self.terminal.middle_of_screen_line_number();
let y = cmp::max(0, y.value);
let y = cmp::min(y, RowIndex::from(max_line_number).value);
if self.offset.rows <= y && y <= self.offset.rows + term_height {
self.cursor_position.y = y.saturating_sub(self.offset.rows);
} else if y < RowIndex::from(middle_of_screen_line_number).value {
self.offset.rows = 0;
self.cursor_position.y = y;
} else if y >= RowIndex::from(max_line_number.sub(middle_of_screen_line_number.value)).value
{
self.offset.rows = max_line_number.sub(term_height).value;
self.cursor_position.y = RowIndex::new(y.saturating_sub(self.offset.rows)).value;
} else {
self.offset.rows =
RowIndex::new(y - RowIndex::from(middle_of_screen_line_number).value).value;
self.cursor_position.y = RowIndex::from(middle_of_screen_line_number).value;
}
}
fn move_cursor_to_position_x(&mut self, x: usize) {
let term_width = self.terminal.size().width as usize;
let x = cmp::max(0, x);
if x > term_width {
self.cursor_position.x = term_width.saturating_sub(1);
self.offset.columns = x
.saturating_sub(term_width)
.saturating_sub(self.offset.columns)
.saturating_add(1);
} else {
self.cursor_position.x = x;
self.offset.columns = 0;
}
}
fn is_dirty(&self) -> bool {
self.last_saved_hash != self.document.hashed()
}
fn undo_last_operation(&mut self) {
if let Some(last_op_undone) = self
.history
.last_operation_reversed(&self.document.row_lengths())
{
match last_op_undone.op_type {
OperationType::Delete => self.document.delete_string(
last_op_undone.content.as_str(),
last_op_undone.start_position.x,
RowIndex::new(last_op_undone.start_position.y),
),
OperationType::Insert => self.document.insert_string(
last_op_undone.content.as_str(),
last_op_undone.start_position.x,
RowIndex::new(last_op_undone.start_position.y),
),
}
self.goto_position(last_op_undone.end_position(&self.document.row_lengths()));
}
}
fn get_x_index_of_currently_selected_suggestion(&self) -> usize {
let mut x_index_of_currently_selected_suggestion = SEARCH_PREFIX.to_string().len();
let sep_len = AUTOCOMPLETION_SUGGESTIONS_SEPARATOR.to_string().len();
for (i, suggestion) in self.command_suggestions.iter().enumerate() {
if i >= self.current_autocompletion_index {
break;
}
x_index_of_currently_selected_suggestion += suggestion.len() + sep_len;
}
x_index_of_currently_selected_suggestion
}
fn refresh_screen(&mut self) -> Result<(), std::io::Error> {
self.terminal.hide_cursor();
if !self.should_quit {
if self.alternate_screen {
self.terminal.clear_all();
self.terminal.to_alternate_screen();
self.draw_help_screen();
} else {
self.terminal.to_main_screen();
self.draw_rows();
}
self.draw_status_bar();
self.draw_message_bar();
if self.alternate_screen {
self.terminal.set_cursor_position_in_text_area(
&Position::top_left(),
self.row_prefix_length,
);
} else if self.is_receiving_command() {
if self.is_autocompleting_command() {
self.terminal.set_cursor_position_anywhere(&Position {
x: self.get_x_index_of_currently_selected_suggestion(),
y: self.terminal.size().height as usize,
});
} else {
self.terminal.set_cursor_position_anywhere(&Position {
x: self.command_buffer.len(),
y: self.terminal.size().height as usize,
});
}
} else {
self.terminal.set_cursor_position_in_text_area(
&self.cursor_position,
self.row_prefix_length,
);
}
}
self.terminal.show_cursor();
self.terminal.flush()
}
fn generate_status(&self) -> String {
let dirty_marker = if self.is_dirty() { " +" } else { "" };
let left_status = format!(
"[{}]{} {}",
self.document
.filename
.as_ref()
.unwrap_or(&PathBuf::from("No Name"))
.to_str()
.unwrap_or_default(),
dirty_marker,
self.mode
);
let stats = if self.config.display_stats {
format!(
"[{}L/{}W]",
self.document.last_line_number().value,
self.document.num_words()
)
} else {
"".to_string()
};
let position = format!(
"Ln {}, Col {}",
self.current_line_number().value,
self.cursor_position
.x
.saturating_add(self.offset.columns)
.saturating_add(1),
);
let right_status = format!("{} {}", stats, position);
let right_status = right_status.trim_start();
let spaces = " ".repeat(
(self.terminal.size().width as usize)
.saturating_sub(left_status.len())
.saturating_sub(right_status.len()),
);
format!("{}{}{}\r", left_status, spaces, right_status)
}
fn draw_status_bar(&self) {
self.terminal.set_bg_color(STATUS_BG_COLOR);
self.terminal.set_fg_color(STATUS_FG_COLOR);
println!("{}", self.generate_status());
self.terminal.reset_fg_color();
self.terminal.reset_bg_color();
}
fn draw_message_bar(&self) {
self.terminal.clear_current_line();
if self.is_receiving_command() {
if self.is_autocompleting_command() {
print!(":{}\r", self.generate_command_autocompletion_message());
} else {
print!("{}\r", self.command_buffer);
}
} else {
print!("{}\r", self.message);
}
}
fn generate_command_autocompletion_message(&self) -> String {
let mut tokens: Vec<String> = vec![];
for (i, suggestion) in self.command_suggestions.iter().enumerate() {
if i == self.current_autocompletion_index {
tokens.push(utils::red(utils::as_bold(suggestion).as_str()));
} else {
tokens.push(suggestion.to_string());
}
}
tokens.join(AUTOCOMPLETION_SUGGESTIONS_SEPARATOR.to_string().as_str())
}
fn display_message(&mut self, message: String) {
self.message = message;
}
fn reset_message(&mut self) {
self.message = String::from("");
}
fn display_welcome_message(&self) {
let term_width = self.terminal.size().width as usize;
let welcome_msg = format!("{} v{}", PKG, utils::bo_version());
let padding_len = term_width
.saturating_sub(welcome_msg.chars().count())
.saturating_sub(2) .saturating_div(2);
let padding = String::from(" ").repeat(padding_len);
let mut padded_welcome_message = format!("~ {}{}{}", padding, welcome_msg, padding);
padded_welcome_message.truncate(term_width); println!("{}\r", padded_welcome_message);
}
#[allow(clippy::cast_possible_truncation)]
fn draw_help_screen(&mut self) {
let help_text_lines = self.help_message.split('\n');
let help_text_lines_count = help_text_lines.count();
let term_height = self.terminal.size().height;
let v_padding = (term_height
.saturating_sub(2)
.saturating_sub(help_text_lines_count as u16))
.saturating_div(2);
let max_line_length = self.help_message.split('\n').map(str::len).max().unwrap();
let h_padding = " ".repeat((self.terminal.size().width as usize - max_line_length) / 2);
for _ in 0..=v_padding {
println!("\r");
}
for line in self.help_message.split('\n') {
println!("{}{}\r", h_padding, line);
}
for _ in 0..=v_padding {
println!("\r");
}
if (v_padding + help_text_lines_count as u16 + v_padding) == (term_height - 1) {
println!("\r");
}
self.display_message("Press q to quit".to_string());
}
fn draw_rows(&self) {
let term_height = self.terminal.size().restrict_to_text_area().height;
for terminal_row_idx_val in self.offset.rows..(term_height as usize + self.offset.rows) {
let terminal_row_idx = RowIndex::new(terminal_row_idx_val);
let line_number = LineNumber::from(terminal_row_idx);
self.terminal.clear_current_line();
if let Some(row) = self.get_row(terminal_row_idx) {
self.draw_row(row, line_number);
} else if line_number == self.terminal.middle_of_screen_line_number()
&& self.document.filename.is_none()
&& self
.get_row(RowIndex::new(0))
.unwrap_or(&Row::default())
.is_empty()
{
self.display_welcome_message();
} else {
println!("~\r");
}
}
}
fn draw_row(&self, row: &Row, line_number: LineNumber) {
let row_visible_start = self.offset.columns;
let mut row_visible_end = self.terminal.size().width as usize + self.offset.columns;
if self.row_prefix_length > 0 {
row_visible_end = row_visible_end
.saturating_sub(self.row_prefix_length as usize)
.saturating_sub(1);
}
let rendered_row = row.render(
row_visible_start,
row_visible_end,
line_number.value,
self.row_prefix_length as usize,
);
println!("{}\r", rendered_row);
}
}
#[cfg(test)]
#[path = "./editor_test.rs"]
mod editor_test;