pub(crate) mod line_wrapper;
pub mod status_line;
pub mod theme;
#[cfg(feature = "syntax-highlighting")]
pub use crate::syntax_higlighting::SyntaxHighlighter;
use crate::{helper::max_col, internal::InternalLine, state::EditorState, EditorMode, Index2};
use line_wrapper::LineWrapper;
use ratatui::{prelude::*, widgets::Widget};
pub use status_line::EditorStatusLine;
use std::cmp::min;
use theme::EditorTheme;
pub struct EditorView<'a, 'b> {
pub(crate) state: &'a mut EditorState,
pub(crate) theme: EditorTheme<'b>,
pub(crate) wrap: bool,
#[cfg(feature = "syntax-highlighting")]
pub(crate) syntax_highlighter: Option<SyntaxHighlighter>,
}
impl<'a, 'b> EditorView<'a, 'b> {
#[must_use]
pub fn new(state: &'a mut EditorState) -> Self {
Self {
state,
theme: EditorTheme::default(),
wrap: true,
#[cfg(feature = "syntax-highlighting")]
syntax_highlighter: None,
}
}
#[must_use]
pub fn theme(mut self, theme: EditorTheme<'b>) -> Self {
self.theme = theme;
self
}
#[cfg(feature = "syntax-highlighting")]
#[must_use]
pub fn syntax_highlighter(mut self, syntax_highlighter: Option<SyntaxHighlighter>) -> Self {
self.syntax_highlighter = syntax_highlighter;
self
}
#[must_use]
pub fn wrap(mut self, wrap: bool) -> Self {
self.wrap = wrap;
self
}
#[must_use]
pub fn get_state(&'a self) -> &'a EditorState {
self.state
}
#[must_use]
pub fn get_state_mut(&'a mut self) -> &'a mut EditorState {
self.state
}
}
impl Widget for EditorView<'_, '_> {
fn render(self, area: Rect, buf: &mut Buffer) {
buf.set_style(area, self.theme.base);
let area = match &self.theme.block {
Some(b) => {
let inner_area = b.inner(area);
b.clone().render(area, buf);
inner_area
}
None => area,
};
let [main, status] = Layout::vertical([
Constraint::Min(0),
Constraint::Length(u16::from(self.theme.status_line.is_some())),
])
.areas(area);
let width = main.width as usize;
let height = main.height as usize;
let lines = &self.state.lines;
let cursor = displayed_cursor(self.state);
self.state.view.set_editor_to_textarea_offset(area);
let size = (width, height);
let offset = self
.state
.view
.update_viewport_offset(size, cursor, lines, self.wrap);
let mut search_selection = None;
if self.state.mode == EditorMode::Search {
search_selection = self.state.search.selected_range();
};
let selections = vec![&self.state.selection, &search_selection];
let mut y = (main.top() as usize) as u16;
let mut num_rows = 0;
for (i, line) in lines.iter_row().skip(offset.y).enumerate() {
let row_index = offset.y + i;
num_rows += 1;
let internal_line = InternalLine::new(
line,
self.theme.base,
self.theme.selection_style,
offset.y + i,
offset.x,
);
#[cfg(feature = "syntax-highlighting")]
let spans = {
if let Some(syntax_highlighter) = &self.syntax_highlighter {
internal_line.into_highlighted_spans(&selections, syntax_highlighter)
} else {
internal_line.into_spans(&selections)
}
};
#[cfg(not(feature = "syntax-highlighting"))]
let spans = { internal_line.into_spans(&selections) };
let (line_widths, wrapped_spans) = if self.wrap {
LineWrapper::wrap_spans(spans, main.width as usize)
} else {
let spans_width = spans_width(&spans);
(vec![spans_width], vec![spans])
};
let line_count = wrapped_spans.len();
let mut y_line = y;
for (i, span) in wrapped_spans.into_iter().enumerate() {
let area = Rect::new(main.left(), y_line, main.width, main.height);
span.into_iter().collect::<Line>().render(area, buf);
if i + 1 < line_count {
y_line += 1;
}
if y_line >= main.bottom() {
break;
}
}
if row_index == cursor.row {
let relative_position = LineWrapper::find_position(&line_widths, cursor.col);
let relative_position_col = relative_position.col.saturating_sub(offset.x);
let x_cursor = main.left() + min(width, relative_position_col) as u16;
let y_cursor = y + relative_position.row as u16;
if let Some(cell) = buf.cell_mut(Position::new(x_cursor, y_cursor)) {
cell.set_style(self.theme.cursor_style);
}
}
y = y_line + 1;
if y >= main.bottom() {
break;
}
}
if num_rows == 0 {
if let Some(cell) = buf.cell_mut(Position::new(main.left(), main.top())) {
cell.set_style(self.theme.cursor_style);
}
}
self.state.view.update_num_rows(num_rows);
if let Some(s) = self.theme.status_line {
s.mode(self.state.mode.name())
.search(if self.state.mode == EditorMode::Search {
Some(self.state.search_pattern())
} else {
None
})
.render(status, buf);
}
}
}
fn crop_first(s: &str, pos: usize) -> &str {
match s.char_indices().nth(pos) {
Some((pos, _)) => &s[pos..],
None => "",
}
}
pub(crate) fn spans_width(spans: &[Span]) -> usize {
spans.iter().fold(0, |sum, span| sum + span.width())
}
fn displayed_cursor(state: &EditorState) -> Index2 {
let max_col = max_col(&state.lines, &state.cursor, state.mode);
Index2::new(state.cursor.row, state.cursor.col.min(max_col))
}