use crate::{
buffer::Buffer,
file_io::try_get_git_hash,
highlight::{HLBlock, HLEnd},
language::OrinLanguage,
};
use anyhow::Result;
use crossterm::{
cursor::{Hide, MoveDown, MoveTo, MoveToColumn},
execute,
style::{Color, Print, SetBackgroundColor, SetForegroundColor},
};
use ropey::RopeSlice;
use std::{
io::{StdoutLock, stdout},
path::PathBuf,
};
use tree_sitter::{Parser, Tree};
pub struct ViewBox {
pub buffer: Buffer,
path: Option<PathBuf>,
pub git_hash: Option<String>,
pub parser: Option<(Parser, OrinLanguage)>,
pub parse_tree: Option<Tree>,
pub x: u16,
pub y: u16,
pub top: usize,
pub height: u16,
pub left: usize,
pub width: u16,
}
impl ViewBox {
pub fn new(cols: u16, rows: u16, x: u16, y: u16) -> Self {
Self {
buffer: Buffer::new(),
path: None,
git_hash: None,
parser: None,
parse_tree: None,
x,
y,
top: 0,
height: rows,
left: 0,
width: cols - 1,
}
}
pub fn adjust(&mut self) -> bool {
let col = self.buffer.get_col();
let row = self.buffer.get_row();
let mut adjusted = false;
if self.top > row {
self.top = row;
adjusted = true;
} else if self.top + self.height as usize <= row {
self.top = row - self.height as usize + 1;
adjusted = true;
}
let left_padding = self.left_padding();
if self.left > col {
self.left = col;
adjusted = true;
} else if self.left + (self.width as usize) < col + left_padding {
self.left = col + left_padding - self.width as usize;
adjusted = true;
}
if adjusted {
self.buffer.update_list_set(.., true);
}
adjusted
}
fn write_buffer(&self, stdout: &mut StdoutLock, left_padding: usize) -> Result<()> {
let lines = self
.buffer
.rope
.lines()
.zip(self.buffer.lines_for_updating.iter())
.enumerate()
.skip(self.top)
.take(self.height.into());
execute!(stdout, Hide, MoveTo(self.x, self.y))?;
let mut padding_buffer = String::with_capacity(left_padding);
let clear_str: String = (0..=self.width).map(|_| ' ').collect();
let maybe_len_lines = u16::try_from(lines.len()).ok();
if let Some(_tree) = self.parse_tree.as_ref()
&& let Some((_parser, language)) = self.parser.as_ref()
&& let Some(path) = self.path.as_ref()
&& let Some(ex) = path.extension()
&& let Some(ex) = ex.to_str()
&& language.extensions.contains(&ex.to_string())
{
self.print_line_hl(
lines,
self.highlight(),
stdout,
&mut padding_buffer,
left_padding,
&clear_str,
);
} else {
self.print_lines_colorless(
lines,
stdout,
&mut padding_buffer,
left_padding,
&clear_str,
);
}
if let Some(len_lines) = maybe_len_lines
&& len_lines < self.height
{
execute!(stdout, MoveTo(self.x, self.y + len_lines))?;
(len_lines..self.height).for_each(|_| {
execute!(stdout, Print(&clear_str), MoveDown(1), MoveToColumn(self.x))
.expect("Crossterm clearing trailing lines failed");
});
}
Ok(())
}
fn print_line_hl<'b>(
&self,
lines: impl Iterator<Item = (usize, (RopeSlice<'b>, &'b bool))>,
hl_lines: Vec<Vec<HLBlock>>,
stdout: &mut StdoutLock,
padding_buffer: &mut String,
left_padding: usize,
clear_str: &str,
) {
let hl_lines = hl_lines.into_iter().skip(self.top).take(self.height.into());
let lines = lines
.zip(hl_lines)
.map(|((line_num, (line, should_update)), hl_blocks)| {
(line_num, line, hl_blocks, should_update)
});
lines.for_each(|(line_num, line, hl_blocks, should_update)| {
if !should_update {
execute!(stdout, MoveDown(1)).expect("Crossterm MoveDown command failed");
return;
}
Self::clear_line(clear_str, stdout);
self.print_padding(padding_buffer, left_padding, line_num, stdout);
let line_len = Self::calculate_total_line_len(line);
if line_len == 0 {
execute!(stdout, MoveToColumn(self.x), MoveDown(1))
.expect("Crossterm padding buffer print failed");
return;
}
let line = line.to_string();
let last_col = self.last_col(left_padding, line_len);
let hl_blocks = self.crop_hl_blocks(&hl_blocks, last_col, line_len);
if hl_blocks.is_empty() {
return;
}
self.print_hl_blocks(&hl_blocks, &line, stdout);
});
}
fn crop_hl_blocks(
&self,
hl_blocks: &[HLBlock],
last_col: usize,
line_len: usize,
) -> Vec<HLBlock> {
let mut hl_blocks: Vec<HLBlock> = hl_blocks
.iter()
.filter(|hl| {
hl.start <= last_col
&& match hl.end {
HLEnd::Bounded(end) => end >= self.left,
HLEnd::EndOfLine => line_len >= self.left,
}
})
.map(std::clone::Clone::clone)
.collect();
if hl_blocks.is_empty() {
return vec![];
}
hl_blocks[0].start = usize::max(self.left, hl_blocks[0].start);
if let Some(block) = hl_blocks.last_mut() {
match block.end {
HLEnd::Bounded(end) => block.end = HLEnd::Bounded(usize::min(last_col, end)),
HLEnd::EndOfLine => block.end = HLEnd::Bounded(last_col),
}
}
hl_blocks
}
fn print_hl_blocks(&self, hl_blocks: &[HLBlock], line: &str, stdout: &mut StdoutLock) {
for hl in hl_blocks {
let text = hl.slice_text(line);
execute!(
stdout,
SetForegroundColor(hl.fg_color),
SetBackgroundColor(hl.bg_color),
Print(text)
)
.expect("Crossterm print hl block command failed");
}
execute!(stdout, MoveToColumn(self.x), MoveDown(1))
.expect("Crossterm reset command failed");
}
fn print_lines_colorless<'b>(
&self,
lines: impl Iterator<Item = (usize, (RopeSlice<'b>, &'b bool))>,
stdout: &mut StdoutLock,
padding_buffer: &mut String,
left_padding: usize,
clear_str: &str,
) {
lines.for_each(|(line_num, (line, should_update))| {
if !should_update {
execute!(stdout, MoveDown(1)).expect("Crossterm MoveDown command failed");
return;
}
Self::clear_line(clear_str, stdout);
self.print_padding(padding_buffer, left_padding, line_num, stdout);
let line_len = Self::calculate_total_line_len(line);
if line_len == 0 {
execute!(stdout, MoveToColumn(self.x), MoveDown(1))
.expect("Crossterm padding buffer print failed");
return;
}
let line = self.slice_line(line, line_len);
execute!(
stdout,
SetForegroundColor(Color::Grey),
Print(&line),
MoveToColumn(self.x),
MoveDown(1)
)
.expect("Crossterm print line command failed");
});
}
fn clear_line(clear_str: &str, stdout: &mut StdoutLock) {
execute!(stdout, Print(&clear_str)).unwrap();
}
fn print_padding(
&self,
padding_buffer: &mut String,
left_padding: usize,
line_num: usize,
stdout: &mut StdoutLock,
) {
let line_num = line_num.to_string();
for _ in 0..left_padding - line_num.len() - 1 {
padding_buffer.push(' ');
}
padding_buffer.push_str(&line_num);
padding_buffer.push(' ');
execute!(
stdout,
SetForegroundColor(Color::Grey),
MoveToColumn(self.x),
Print(padding_buffer.clone()),
)
.expect("Crossterm padding buffer print failed");
padding_buffer.clear();
}
fn calculate_total_line_len(line: RopeSlice) -> usize {
let mut total_line_len = line.len_chars();
if total_line_len > 0
&& let Some(c) = line.get_char(total_line_len - 1)
&& c == '\n'
{
total_line_len -= 1;
}
total_line_len
}
fn last_col(&self, left_padding: usize, line_len: usize) -> usize {
let display_line_len = self.width as usize - left_padding;
usize::min(self.left + display_line_len, line_len)
}
fn slice_line(&self, line: RopeSlice, last_col: usize) -> String {
if self.left >= last_col {
String::new()
} else {
line.slice(self.left..last_col)
.to_string()
.trim_matches('\n')
.to_string()
}
}
pub fn flush(&self, adjusted: bool) -> Result<()> {
let mut stdout = stdout().lock();
let left_padding = self.left_padding();
if self.buffer.has_changed || adjusted {
self.write_buffer(&mut stdout, left_padding)?;
}
Ok(())
}
pub fn left_padding(&self) -> usize {
(self.top + self.height as usize).to_string().len() + 1
}
pub const fn _get_lower_right(&self) -> (u16, u16) {
(self.x + self.width, self.y + self.height)
}
pub const fn get_lower_left(&self) -> (u16, u16) {
(self.x, self.y + self.height)
}
pub const fn get_upper_right(&self) -> (u16, u16) {
(self.x + self.width, self.y)
}
pub fn cursor_position(&self) -> (u16, u16) {
let left_padding = self.left_padding();
let buffer_col = self.buffer.get_col();
let buffer_row = self.buffer.get_row();
let absolute_row = self.y + u16::try_from(buffer_row - self.top).unwrap_or(0);
let absolute_col = self.x
+ u16::min(
u16::try_from(buffer_col - self.left + left_padding).unwrap_or(0),
self.width,
);
(absolute_col, absolute_row)
}
pub fn set_path(&mut self, path: Option<PathBuf>) {
if let Some(path) = &path
&& let Some(ext) = path.extension()
&& let Some(ext) = ext.to_str()
&& let Some(language) = OrinLanguage::from_ext(ext)
{
let mut parser = Parser::new();
parser
.set_language(&language.lang)
.expect("Failed to load C parser");
self.parser = Some((parser, language));
}
self.git_hash = try_get_git_hash(path.as_ref());
self.path = path;
}
pub const fn path(&self) -> Option<&PathBuf> {
self.path.as_ref()
}
}