use crate::{
DEBUG, buffer::Buffer, io::try_get_git_hash, log, mode::Mode, status_bar::StatusBar,
undo::UndoTree, view_box::ViewBox,
};
use anyhow::Result;
use crossterm::{
cursor::{MoveTo, MoveToColumn, MoveToRow, SetCursorStyle, Show},
execute,
style::{Color, Print, ResetColor, SetForegroundColor},
terminal::{
Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode,
enable_raw_mode,
},
};
use ropey::Rope;
use std::{
io::{Write, stdout},
path::PathBuf,
};
#[derive(Debug)]
pub struct View {
boxes: Vec<ViewBox>,
pub cursor: usize,
width: u16,
height: u16,
}
impl View {
pub fn new(cols: u16, rows: u16) -> Self {
Self {
boxes: vec![ViewBox::new(cols, rows - 1, 0, 0)],
cursor: 0,
width: cols, height: rows - 1,
}
}
pub fn get_buffer_mut(&mut self) -> &mut Buffer {
&mut self.boxes[self.cursor].buffer
}
pub fn get_buffer(&self) -> &Buffer {
&self.boxes[self.cursor].buffer
}
pub fn get_view_box(&mut self) -> &mut ViewBox {
&mut self.boxes[self.cursor]
}
#[allow(clippy::too_many_arguments)]
pub fn flush(
&self,
status_bar: &StatusBar,
mode: &Mode,
chained: &[char],
count: u16,
register: char,
adjusted: bool,
) -> Result<()> {
let mut errors = self.boxes.iter().enumerate().filter_map(|(i, view_box)| {
let adjusted = adjusted && i == self.cursor;
view_box.flush(adjusted).err()
});
if let Some(err) = errors.next() {
return Err(err);
}
let status_message = match (mode, self.get_path()) {
(Mode::Meta | Mode::Search, _) => status_bar.buffer(),
(Mode::Normal, Some(path)) => {
let info_str = "Editing File: ".to_string();
let file_size = std::fs::read(path)?.len().to_string();
let path = path.to_string_lossy();
let count_str = if count == 1 {
String::new()
} else {
count.to_string()
};
let reg_str = if register == '\"' {
String::new()
} else {
format!("\"{register}")
};
let chained_str = chained.iter().collect::<String>();
let git_hash = self.get_git_hash().unwrap_or("");
let middle_buffer = (0..(self.width as usize)
- info_str.len()
- path.len()
- 2 - 3 - 1 - file_size.len()
- reg_str.len()
- count_str.len()
- chained_str.len()
- git_hash.len())
.map(|_| ' ')
.collect::<String>();
format!(
"{info_str}\"{path}\" {file_size}b {reg_str}{count_str} {chained_str}{middle_buffer}{git_hash}",
)
}
(Mode::Normal, None) => {
let info_str = "-- Unattached Buffer -- ".to_string();
let count_str = if count == 1 {
String::new()
} else {
count.to_string()
};
let reg_str = if register == '\"' {
String::new()
} else {
format!("\"{register}")
};
let chained_str = chained.iter().collect::<String>();
format!("{info_str}{count_str}{reg_str}{chained_str}")
}
(Mode::Insert, _) => "-- INSERT --".into(),
(Mode::Visual, _) => "-- VISUAL --".into(),
};
execute!(
stdout(),
SetForegroundColor(Color::White),
MoveTo(0, self.height + 1),
Clear(ClearType::CurrentLine),
Print(status_message)
)?;
let (new_col, new_row) = if matches!(mode, Mode::Meta | Mode::Search) {
(status_bar.idx(), self.height + 1)
} else {
let view_box = &self.boxes[self.cursor];
view_box.cursor_position()
};
execute!(stdout(), MoveToColumn(new_col), MoveToRow(new_row), Show)?;
stdout().flush()?;
Ok(())
}
}
impl View {
pub fn position_of_box<P>(&self, predicate: P) -> Option<usize>
where
P: FnMut(&ViewBox) -> bool,
{
self.boxes.iter().position(predicate)
}
pub fn position_view_box_down(&mut self) -> Option<usize> {
let view_box = self.get_view_box();
let (x, y) = view_box.get_lower_left();
let predicate = |view_box: &ViewBox| -> bool { view_box.x == x && view_box.y == y };
self.position_of_box(predicate)
}
pub fn position_view_box_up(&mut self) -> Option<usize> {
let view_box = self.get_view_box();
let (x, y) = (view_box.x, view_box.y);
let predicate =
|view_box: &ViewBox| -> bool { view_box.x == x && view_box.y + view_box.height == y };
self.position_of_box(predicate)
}
pub fn position_view_box_left(&mut self) -> Option<usize> {
let view_box = self.get_view_box();
let (x, y) = (view_box.x, view_box.y);
let predicate =
|view_box: &ViewBox| -> bool { view_box.y == y && view_box.x + view_box.width == x };
self.position_of_box(predicate)
}
pub fn position_view_box_right(&mut self) -> Option<usize> {
let view_box = self.get_view_box();
let (x, y) = view_box.get_upper_right();
let predicate = |view_box: &ViewBox| -> bool { view_box.y == y && view_box.x == x };
self.position_of_box(predicate)
}
pub fn delete_curr_view_box(&mut self) {
let mut down = self.position_view_box_down();
let mut up = self.position_view_box_up();
let view_box = self.boxes.remove(self.cursor);
if let Some(ref mut down) = down
&& *down > self.cursor
{
*down -= 1;
}
if let Some(ref mut up) = up
&& *up > self.cursor
{
*up -= 1;
}
self.cursor = usize::max(self.cursor, 1) - 1;
match (down, up) {
(_, Some(up_i)) => {
let up_box = &mut self.boxes[up_i];
up_box.height += view_box.height;
self.cursor = up_i;
}
(Some(down_i), None) => {
let down_box = &mut self.boxes[down_i];
down_box.y = view_box.y;
down_box.height += view_box.height;
self.cursor = down_i;
}
(None, None) => {}
}
let view_box = self.get_view_box();
view_box.buffer.has_changed = true;
}
pub fn split_view_box_vertical(&mut self, idx: usize) {
let view_box = &mut self.boxes[idx];
let half_height = view_box.height / 2;
let half_y = half_height + view_box.y;
let mut new_view_box = ViewBox::new(view_box.width, half_height, view_box.x, half_y);
let original_height = view_box.height;
view_box.height = half_height;
if !original_height.is_multiple_of(2) {
new_view_box.height += 1;
}
self.boxes.push(new_view_box);
}
pub fn split_view_box_horizontal(&mut self, idx: usize) {
let view_box = &mut self.boxes[idx];
let half_width = view_box.width / 2;
let half_x = half_width + view_box.x;
let mut new_view_box = ViewBox::new(half_width, view_box.height, half_x, view_box.y);
let original_width = view_box.width;
view_box.width = half_width;
if !original_width.is_multiple_of(2) {
new_view_box.width += 1;
}
self.boxes.push(new_view_box);
}
}
impl View {
pub fn replace_buffer_contents(
&mut self,
contents: &impl ToString,
cursor: usize,
undo_tree: &mut UndoTree,
) {
let str = contents.to_string();
let anchor = self.cursor;
self.cursor = cursor;
let buffer = self.get_buffer_mut();
buffer.replace_contents(str, undo_tree);
self.cursor = anchor;
}
pub fn load_file(&mut self) -> Result<()> {
if let Some(path) = self.get_path().cloned() {
let buffer = self.get_buffer_mut();
if !std::fs::exists(&path)? {
std::fs::write(path, buffer.rope.to_string())?;
return Ok(());
}
let contents = std::fs::read_to_string(path)?;
buffer.rope = Rope::from(contents);
buffer.lines_for_updating = (0..buffer.len()).map(|_| true).collect::<Vec<bool>>();
buffer.cursor = usize::min(buffer.cursor, buffer.rope.len_chars());
buffer.has_changed = true;
}
Ok(())
}
pub fn write(&self) -> Result<()> {
let buffer = self.get_buffer().to_string();
if let Some(path) = self.get_path() {
std::fs::write(path, buffer)?;
} else {
log!("WARNING: Cannot Write Unattached Buffer");
}
Ok(())
}
pub fn adjust(&mut self) -> bool {
let view_box = self.get_view_box();
view_box.adjust()
}
pub const fn boxes_len(&self) -> usize {
self.boxes.len()
}
pub fn set_path(&mut self, path: Option<PathBuf>) {
let git_hash = try_get_git_hash(path.as_ref());
let view_box = &mut self.boxes[self.cursor];
view_box.git_hash = git_hash;
view_box.path = path;
}
pub fn get_path(&self) -> Option<&PathBuf> {
let view_box = &self.boxes[self.cursor];
view_box.path.as_ref()
}
pub fn get_git_hash(&self) -> Option<&str> {
let view_box = &self.boxes[self.cursor];
view_box.git_hash.as_deref()
}
}
pub fn cleanup() -> Result<()> {
disable_raw_mode()?;
execute!(
stdout(),
ResetColor,
Clear(ClearType::All),
SetCursorStyle::SteadyBlock,
LeaveAlternateScreen
)?;
Ok(())
}
pub fn setup(rows: u16, cols: u16) -> Result<()> {
execute!(
stdout(),
EnterAlternateScreen,
Clear(ClearType::All),
MoveToRow(0),
SetForegroundColor(Color::Blue),
)?;
for row in 0..rows {
execute!(stdout(), MoveTo(0, row), Print(" ".repeat(cols as usize)))?;
}
execute!(stdout(), MoveTo(0, 0))?;
for row in 0..rows {
execute!(stdout(), MoveTo(0, row), Print(" ".repeat(cols as usize)))?;
}
execute!(stdout(), MoveTo(0, 0))?;
enable_raw_mode()?;
Ok(())
}