use crate::{
minus_core::{self, utils::LinesRowMap},
LineNumbers,
};
#[cfg(feature = "search")]
use regex::Regex;
use std::borrow::Cow;
#[cfg(feature = "search")]
use {crate::search, std::collections::BTreeSet};
pub type Row = String;
pub type Rows = Vec<String>;
pub type Line<'a> = &'a str;
pub type TextBlock<'a> = &'a str;
pub type OwnedTextBlock = String;
pub struct Screen {
pub(crate) orig_text: OwnedTextBlock,
pub(crate) formatted_lines: Rows,
pub(crate) line_count: usize,
pub(crate) max_line_length: usize,
pub(crate) unterminated: usize,
pub(crate) line_wrapping: bool,
}
impl Screen {
#[must_use]
pub fn formatted_lines_count(&self) -> usize {
self.formatted_lines.len()
}
#[must_use]
pub const fn line_count(&self) -> usize {
self.line_count
}
pub(crate) fn get_formatted_lines_with_bounds(&self, start: usize, end: usize) -> &[Row] {
if start >= self.formatted_lines_count() || start > end {
&[]
} else if end >= self.formatted_lines_count() {
&self.formatted_lines[start..]
} else {
&self.formatted_lines[start..end]
}
}
#[must_use]
pub const fn get_max_line_length(&self) -> usize {
self.max_line_length
}
pub(crate) fn push_screen_buf(
&mut self,
text: TextBlock,
line_numbers: LineNumbers,
cols: u16,
#[cfg(feature = "search")] search_term: &Option<Regex>,
) -> FormatResult {
let clean_append = self.orig_text.ends_with('\n') || self.orig_text.is_empty();
let old_lc = self.line_count();
self.formatted_lines
.truncate(self.formatted_lines.len() - self.unterminated);
let append_props = {
let attachment = if clean_append {
None
} else {
self.orig_text.lines().last()
};
let formatted_lines_count = self.formatted_lines.len();
let append_opts = FormatOpts {
buffer: &mut self.formatted_lines,
text,
attachment,
line_numbers,
formatted_lines_count,
lines_count: old_lc,
prev_unterminated: self.unterminated,
cols: cols.into(),
line_wrapping: self.line_wrapping,
#[cfg(feature = "search")]
search_term,
};
format_text_block(append_opts)
};
self.orig_text.push_str(text);
let (num_unterminated, lines_formatted, max_line_length) = (
append_props.num_unterminated,
append_props.lines_formatted,
append_props.max_line_length,
);
self.line_count = old_lc + lines_formatted.saturating_sub(usize::from(!clean_append));
if max_line_length > self.max_line_length {
self.max_line_length = max_line_length;
}
self.unterminated = num_unterminated;
append_props
}
}
impl Default for Screen {
fn default() -> Self {
Self {
line_wrapping: true,
orig_text: String::with_capacity(100 * 1024),
formatted_lines: Vec::with_capacity(500 * 1024),
line_count: 0,
max_line_length: 0,
unterminated: 0,
}
}
}
pub(crate) trait AppendableBuffer {
fn append_to_buffer(&mut self, other: &mut Rows);
fn extend_buffer<I>(&mut self, other: I)
where
I: IntoIterator<Item = Row>;
}
impl AppendableBuffer for Rows {
fn append_to_buffer(&mut self, other: &mut Rows) {
self.append(other);
}
fn extend_buffer<I>(&mut self, other: I)
where
I: IntoIterator<Item = Row>,
{
self.extend(other);
}
}
impl AppendableBuffer for &mut Rows {
fn append_to_buffer(&mut self, other: &mut Rows) {
self.append(other);
}
fn extend_buffer<I>(&mut self, other: I)
where
I: IntoIterator<Item = Row>,
{
self.extend(other);
}
}
pub(crate) struct FormatOpts<'a, B>
where
B: AppendableBuffer,
{
pub buffer: B,
pub text: TextBlock<'a>,
pub attachment: Option<TextBlock<'a>>,
pub line_numbers: LineNumbers,
pub lines_count: usize,
pub formatted_lines_count: usize,
pub cols: usize,
pub prev_unterminated: usize,
#[cfg(feature = "search")]
pub search_term: &'a Option<regex::Regex>,
pub line_wrapping: bool,
}
#[derive(Debug)]
pub(crate) struct FormatResult {
pub lines_formatted: usize,
pub rows_formatted: usize,
pub num_unterminated: usize,
#[cfg(feature = "search")]
pub append_search_idx: BTreeSet<usize>,
pub lines_to_row_map: LinesRowMap,
pub max_line_length: usize,
pub clean_append: bool,
}
#[allow(clippy::too_many_lines)]
pub(crate) fn format_text_block<B>(mut opts: FormatOpts<'_, B>) -> FormatResult
where
B: AppendableBuffer,
{
let to_format;
if let Some(attached_text) = opts.attachment {
opts.lines_count = opts.lines_count.saturating_sub(1);
opts.formatted_lines_count = opts
.formatted_lines_count
.saturating_sub(opts.prev_unterminated);
let mut s = String::with_capacity(opts.text.len() + attached_text.len());
s.push_str(attached_text);
s.push_str(opts.text);
to_format = s;
} else {
to_format = opts.text.to_string();
}
let lines = to_format
.lines()
.enumerate()
.collect::<Vec<(usize, &str)>>();
let to_format_size = lines.len();
let mut fr = FormatResult {
lines_formatted: to_format_size,
rows_formatted: 0,
num_unterminated: opts.prev_unterminated,
#[cfg(feature = "search")]
append_search_idx: BTreeSet::new(),
lines_to_row_map: LinesRowMap::new(),
max_line_length: 0,
clean_append: opts.attachment.is_none(),
};
let line_number_digits = minus_core::utils::digits(opts.lines_count + to_format_size);
if lines.is_empty() {
return fr;
}
let mut formatted_row_count = opts.formatted_lines_count;
{
let line_numbers = opts.line_numbers;
let cols = opts.cols;
let lines_count = opts.lines_count;
let line_wrapping = opts.line_wrapping;
#[cfg(feature = "search")]
let search_term = opts.search_term;
let rest_lines =
lines
.iter()
.take(lines.len().saturating_sub(1))
.flat_map(|(idx, line)| {
let fmt_line = formatted_line(
line,
line_number_digits,
lines_count + idx,
line_numbers,
cols,
line_wrapping,
#[cfg(feature = "search")]
formatted_row_count,
#[cfg(feature = "search")]
&mut fr.append_search_idx,
#[cfg(feature = "search")]
search_term,
);
fr.lines_to_row_map.insert(formatted_row_count, true);
formatted_row_count += fmt_line.len();
if lines.len() > fr.max_line_length {
fr.max_line_length = line.len();
}
fmt_line
});
opts.buffer.extend_buffer(rest_lines);
};
let mut last_line = formatted_line(
lines.last().unwrap().1,
line_number_digits,
opts.lines_count + to_format_size - 1,
opts.line_numbers,
opts.cols,
opts.line_wrapping,
#[cfg(feature = "search")]
formatted_row_count,
#[cfg(feature = "search")]
&mut fr.append_search_idx,
#[cfg(feature = "search")]
opts.search_term,
);
fr.lines_to_row_map.insert(formatted_row_count, true);
formatted_row_count += last_line.len();
if lines.last().unwrap().1.len() > fr.max_line_length {
fr.max_line_length = lines.last().unwrap().1.len();
}
#[cfg(feature = "search")]
{
fr.append_search_idx = fr
.append_search_idx
.iter()
.map(|i| opts.formatted_lines_count + i)
.collect();
}
fr.num_unterminated = if opts.text.ends_with('\n') {
0
} else {
last_line.len()
};
opts.buffer.append_to_buffer(&mut last_line);
fr.rows_formatted = formatted_row_count - opts.formatted_lines_count;
fr
}
#[allow(clippy::too_many_arguments)]
#[allow(clippy::uninlined_format_args)]
pub(crate) fn formatted_line<'a>(
line: Line<'a>,
len_line_number: usize,
idx: usize,
line_numbers: LineNumbers,
cols: usize,
line_wrapping: bool,
#[cfg(feature = "search")] formatted_idx: usize,
#[cfg(feature = "search")] search_idx: &mut BTreeSet<usize>,
#[cfg(feature = "search")] search_term: &Option<regex::Regex>,
) -> Rows {
assert!(
!line.contains('\n'),
"Newlines found in appending line {:?}",
line
);
let line_numbers = matches!(line_numbers, LineNumbers::Enabled | LineNumbers::AlwaysOn);
let padding = len_line_number + LineNumbers::EXTRA_PADDING + 1;
let cols_avail = if line_numbers {
cols.saturating_sub(padding + 2)
} else {
cols
};
let mut enumerated_rows = if line_wrapping {
textwrap::wrap(line, cols_avail)
} else {
vec![Cow::from(line)]
}
.into_iter()
.enumerate();
#[cfg_attr(not(feature = "search"), allow(unused_mut))]
#[cfg_attr(not(feature = "search"), allow(unused_variables))]
let mut handle_search = |row: &mut Cow<'a, str>, wrap_idx: usize| {
#[cfg(feature = "search")]
if let Some(st) = search_term.as_ref() {
let (highlighted_row, is_match) = search::highlight_line_matches(row, st, false);
if is_match {
*row.to_mut() = highlighted_row;
search_idx.insert(formatted_idx + wrap_idx);
}
}
};
if line_numbers {
let mut formatted_rows = Vec::with_capacity(256);
let formatter = |row: Cow<'_, str>, is_first_row: bool, idx: usize| {
format!(
"{bold}{number: >len$}{reset} {row}",
bold = if cfg!(not(test)) && is_first_row {
crossterm::style::Attribute::Bold.to_string()
} else {
String::new()
},
number = if is_first_row {
(idx + 1).to_string() + "."
} else {
String::new()
},
len = padding,
reset = if cfg!(not(test)) && is_first_row {
crossterm::style::Attribute::Reset.to_string()
} else {
String::new()
},
row = row
)
};
let first_row = {
#[cfg_attr(not(feature = "search"), allow(unused_mut))]
let mut row = enumerated_rows.next().unwrap().1;
handle_search(&mut row, 0);
formatter(row, true, idx)
};
formatted_rows.push(first_row);
#[cfg_attr(not(feature = "search"), allow(unused_mut))]
#[cfg_attr(not(feature = "search"), allow(unused_variables))]
let rows_left = enumerated_rows.map(|(wrap_idx, mut row)| {
handle_search(&mut row, wrap_idx);
formatter(row, false, 0)
});
formatted_rows.extend(rows_left);
formatted_rows
} else {
#[cfg_attr(not(feature = "search"), allow(unused_variables))]
enumerated_rows
.map(|(wrap_idx, mut row)| {
handle_search(&mut row, wrap_idx);
row.to_string()
})
.collect::<Vec<String>>()
}
}
pub(crate) fn make_format_lines(
text: &String,
line_numbers: LineNumbers,
cols: usize,
line_wrapping: bool,
#[cfg(feature = "search")] search_term: &Option<regex::Regex>,
) -> (Rows, FormatResult) {
let mut buffer = Vec::with_capacity(256);
let format_opts = FormatOpts {
buffer: &mut buffer,
text,
attachment: None,
line_numbers,
formatted_lines_count: 0,
lines_count: 0,
prev_unterminated: 0,
cols,
#[cfg(feature = "search")]
search_term,
line_wrapping,
};
let fr = format_text_block(format_opts);
(buffer, fr)
}
#[cfg(test)]
mod tests;