diff --git a/src/buffer.rs b/src/buffer.rs index e9fa751..dc0cedd 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -18,18 +18,71 @@ */ use std::io::Write; -use std::ops::Range; use std::sync::Arc; use crossterm::{cursor, QueueableCommand}; use parking_lot::Mutex; -use ropey::Rope; +use ropey::{Rope, RopeSlice}; use syntect::easy::ScopeRangeIterator; use syntect::parsing::{BasicScopeStackOp, ParseState, ScopeStack, SyntaxSet}; -use crate::theme::{Style, StyleStore}; +use crate::theme::{Style, StyleStore, StyledString}; use crate::{Cursor, Direction}; +pub struct LineBuf<'a> { + pub buf: Vec, + pub padding: &'a str, + pub remaining_width: usize, + pub styles: &'a mut StyleStore, + pub is_selected: bool, + pub text: RopeSlice<'a>, + pub styled: &'a StyledString, +} + +impl<'a> LineBuf<'a> { + pub fn draw_linenr(&mut self, linenr: usize, width: usize) -> crossterm::Result<()> { + let linenr_style = self.styles.get_scope("ui.linenr").clone(); + + let linenr_style = if self.is_selected { + self.styles.get_scope("ui.linenr.selected").clone() + } else { + linenr_style + }; + + let linenr = if self.text.len_chars() > 0 { + format!("{:>width$} ", linenr) + } else { + // only the last line is empty + format!("{:>width$}", "~") + }; + + linenr_style.print_styled(&mut self.buf, &linenr)?; + self.remaining_width -= width; + + Ok(()) + } + + pub fn draw_text(&mut self, lhs: usize) -> crossterm::Result<()> { + let line_style = if self.is_selected { + self.styles.get_scope("ui.cursorline.primary").clone() + } else { + Default::default() + }; + + let remaining = if lhs < self.remaining_width { + let sub = lhs..self.remaining_width; + self.styled.print_sub(&mut self.buf, sub)? + } else { + self.remaining_width + }; + + let padding = &self.padding[0..remaining]; + line_style.print_styled(&mut self.buf, padding)?; + + Ok(()) + } +} + #[derive(Clone, Debug)] pub struct Buffer { styles: Arc>, @@ -38,7 +91,7 @@ pub struct Buffer { parser: ParseState, style_dirty: bool, style_generation: usize, - styled: Vec)>>, + styled: Vec, } impl AsRef for Buffer { @@ -80,70 +133,33 @@ impl Buffer { self.parse(); let mut styles = self.styles.lock(); - let linenr_style = styles.get_scope("ui.linenr").clone(); - let linenr_width = self.text.len_lines().ilog10() + 1; - let gutter_width = linenr_width + 1; - let text_width = cols as usize - gutter_width as usize; + let linenr_width = (self.text.len_lines().ilog10() + 1) as usize; + let gutter_width = linenr_width as u32 + 1; + let text_width = cols as usize - linenr_width; let line_padding: String = std::iter::repeat(' ').take(text_width).collect(); out.queue(cursor::MoveTo(0, 0))?; - let mut line_buf = Vec::::with_capacity(text_width); - for (row, line) in (0..rows).zip(self.text.lines_at(scroll.line)) { - let row = row as usize + scroll.line; - let is_selected = row == cursor.line; - - let linenr_style = if is_selected { - styles.get_scope("ui.linenr.selected").clone() - } else { - linenr_style - }; - - let linenr = if line.len_chars() > 0 { - format!("{:>width$} ", row, width = linenr_width as usize) - } else { - // only the last line is empty - format!("{:>width$}", "~", width = linenr_width as usize) - }; - - let line_style = if is_selected { - styles.get_scope("ui.cursorline.primary").clone() - } else { - Default::default() - }; - - linenr_style.print_styled(out, &linenr)?; - - let lhs = scroll.column; - let width = line.len_chars(); - let mut remaining = text_width; - line_buf.clear(); - if lhs < width { - let window = text_width.min(width - lhs); - let rhs = lhs + window; - let styled = &self.styled[row]; - for (chunk_style, range) in styled.iter() { - let range = range.start.max(lhs)..range.end.min(rhs); - if range.start < range.end { - // this range is in view so display - let content = line.slice(range.clone()); - let mut style = line_style.clone(); - style.apply(chunk_style); - style.print_styled(&mut line_buf, content)?; - remaining = text_width.saturating_sub(range.end); - } - - if range.end >= rhs { - // past the end of the window so we're done - break; - } - } + for (row, line) in self.text.lines_at(scroll.line).enumerate() { + if row == rows as usize { + break; } - let padding = &line_padding[0..remaining]; - line_style.print_styled(&mut line_buf, padding)?; + let linenr = row as usize + scroll.line; - out.write_all(&line_buf)?; + let mut line = LineBuf { + buf: Vec::with_capacity(text_width * 2), + padding: &line_padding, + remaining_width: cols as usize - 1, + styles: &mut styles, + is_selected: linenr == cursor.line, + text: line, + styled: &self.styled[linenr], + }; + + line.draw_linenr(linenr, linenr_width)?; + line.draw_text(scroll.column)?; + out.write_all(&line.buf)?; out.queue(cursor::MoveToNextLine(1))?; } @@ -213,11 +229,17 @@ impl Buffer { continue; } + let len = range.end - range.start; let style = style_stack.last().cloned().unwrap_or_default(); - styled_buf.push((style, range)); + styled_buf.push((len, style)); } - self.styled.push(styled_buf.clone()); + let line = StyledString { + content: line.to_string(), + styles: styled_buf.clone(), + }; + + self.styled.push(line); } self.style_dirty = false; diff --git a/src/theme.rs b/src/theme.rs index a9758b7..54a4c72 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -17,7 +17,7 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -use std::{collections::HashMap, fmt::Display}; +use std::{collections::HashMap, fmt::Display, ops::Range}; use crossterm::style::Color; use crossterm::QueueableCommand; @@ -122,6 +122,63 @@ impl StyleStore { } } +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct StyledString { + pub content: String, + pub styles: Vec<(usize, Style)>, +} + +impl StyledString { + pub fn new_unstyled(content: String) -> Self { + Self { + styles: vec![(content.len(), Style::default())], + content, + } + } + + pub fn print(&self, out: &mut impl QueueableCommand) -> crossterm::Result<()> { + let mut cursor = 0; + for (size, style) in self.styles.iter() { + let end = cursor + size; + let range = cursor..end; + let sub = &self.content[range]; + style.print_styled(out, sub)?; + cursor = end; + } + + Ok(()) + } + + pub fn print_sub( + &self, + out: &mut impl QueueableCommand, + clip: Range, + ) -> crossterm::Result { + let mut remaining = clip.end - clip.start; + let mut cursor = 0; + for (size, style) in self.styles.iter() { + let end = cursor + size; + let clipped = (cursor.max(clip.start))..(end.min(clip.end)); + cursor = end; + + if clipped.start >= clipped.end { + continue; + } + + remaining -= clipped.end - clipped.start; + + let sub = &self.content[clipped]; + style.print_styled(out, sub)?; + + if cursor >= clip.end { + break; + } + } + + Ok(remaining) + } +} + #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub struct Style { pub fg: Option,