use threadpool::ThreadPool;
use crate::lines_highlighter::{LineAcceptance, LinesHighlighter, Response};
use crate::refiner::Formatter;
use crate::string_future::StringFuture;
#[derive(Debug)]
pub(crate) struct PlusMinusLinesHighlighter {
prefix_length: usize,
texts: Vec<String>,
prefixes: Vec<String>,
last_seen_prefix: Option<String>,
formatter: Formatter,
}
impl LinesHighlighter for PlusMinusLinesHighlighter {
fn consume_line(&mut self, line: &str, thread_pool: &ThreadPool) -> Result<Response, String> {
if line.is_empty() {
return Ok(Response {
line_accepted: LineAcceptance::RejectedDone,
highlighted: self.drain(thread_pool),
});
}
if line.starts_with('\\') {
return self.consume_nnaeof(thread_pool);
}
if line.len() < self.prefix_length {
return Err(format!(
"Line too short, expected 0 or at least {} characters",
self.prefix_length,
));
}
let (prefix, line) = line.split_at(self.prefix_length);
if prefix.chars().all(|c| c == ' ') {
return Ok(Response {
line_accepted: LineAcceptance::RejectedDone,
highlighted: self.drain(thread_pool),
});
}
if prefix.chars().any(|c| ![' ', '-', '+'].contains(&c)) {
return Err(format!(
"Unexpected character in prefix <{prefix}>, only +, - and space allowed: <{prefix}>"
));
}
self.last_seen_prefix = Some(prefix.to_string());
if prefix != self.current_prefix() {
if self.current_prefix().contains('+') {
return Ok(Response {
line_accepted: LineAcceptance::RejectedDone,
highlighted: self.drain(thread_pool),
});
}
self.prefixes.push(prefix.to_string());
self.texts.push(String::new());
assert_eq!(prefix, self.current_prefix());
}
let text = self.texts.last_mut().unwrap();
text.push_str(line);
text.push('\n');
return Ok(Response {
line_accepted: LineAcceptance::AcceptedWantMore,
highlighted: vec![],
});
}
fn consume_eof(&mut self, thread_pool: &ThreadPool) -> Result<Vec<StringFuture>, String> {
if self.last_seen_prefix.is_none() {
return Err("Got EOF without any lines".to_string());
}
return Ok(self.drain(thread_pool));
}
}
impl PlusMinusLinesHighlighter {
#[must_use]
pub(crate) fn from_line(
line: &str,
prefix_length: usize,
formatter: Formatter,
) -> Option<Self> {
if line.len() < prefix_length {
return None;
}
let (prefix, line) = line.split_at(prefix_length);
if !prefix.chars().any(|c| ['-', '+'].contains(&c)) {
return None;
}
return Some(PlusMinusLinesHighlighter {
prefix_length,
texts: vec![line.to_string() + "\n"],
prefixes: vec![prefix.to_string()],
last_seen_prefix: Some(prefix.to_string()),
formatter,
});
}
fn current_prefix(&self) -> &str {
if let Some(prefix) = self.prefixes.last() {
return prefix;
}
return "";
}
fn consume_nnaeof(&mut self, thread_pool: &ThreadPool) -> Result<Response, String> {
if self.last_seen_prefix.is_none() {
return Err(
"Got '\\ No newline at end of file' without being in a +/- section".to_string(),
);
}
let prefix = self.last_seen_prefix.as_ref().unwrap();
if prefix.contains('+') {
let text = self.texts.last_mut().unwrap();
if let Some(without_newline) = text.strip_suffix('\n') {
*text = without_newline.to_string();
} else {
return Err(
"Got + '\\ No newline at end of file' without any newline to remove"
.to_string(),
);
}
return Ok(Response {
line_accepted: LineAcceptance::AcceptedDone,
highlighted: self.drain(thread_pool),
});
}
for (pos, plus_minus_space) in prefix.chars().enumerate() {
if plus_minus_space == ' ' {
continue;
}
let text = self.texts[pos].strip_suffix('\n');
if let Some(text) = text {
self.texts[pos] = text.to_string();
} else {
return Err(
"Got - '\\ No newline at end of file' without any newline to remove"
.to_string(),
);
}
}
return Ok(Response {
line_accepted: LineAcceptance::AcceptedWantMore,
highlighted: vec![],
});
}
#[must_use]
fn drain(&mut self, thread_pool: &ThreadPool) -> Vec<StringFuture> {
if self.texts.iter().all(|flavor| flavor.is_empty()) {
return vec![];
}
let texts = self.texts.clone();
let prefixes = self.prefixes.clone();
let formatter = self.formatter.clone();
self.texts.clear();
self.prefixes.clear();
let return_me = StringFuture::from_function(
move || {
let mut result = String::new();
for line in formatter.format(
&prefixes.iter().map(String::as_str).collect::<Vec<&str>>(),
&texts.iter().map(String::as_str).collect::<Vec<&str>>(),
) {
result.push_str(&line);
result.push('\n');
}
result
},
thread_pool,
);
return vec![return_me];
}
}
#[cfg(test)]
mod tests {
use crate::lines_highlighter::LinesHighlighter;
use crate::refiner::tests::FORMATTER;
use crate::{
line_collector::NO_EOF_NEWLINE_MARKER_HOLDER, lines_highlighter::LineAcceptance,
plusminus_lines_highlighter::PlusMinusLinesHighlighter,
};
use threadpool::ThreadPool;
#[test]
fn test_nnaeol() {
{
let mut no_eof_newline_marker = NO_EOF_NEWLINE_MARKER_HOLDER.lock().unwrap();
*no_eof_newline_marker = Some("\\ No newline at end of file".to_string());
}
let mut test_me =
PlusMinusLinesHighlighter::from_line("+No trailing newline", 1, FORMATTER.clone())
.unwrap();
assert_eq!(test_me.texts, vec!["No trailing newline\n"]);
assert_eq!(test_me.prefixes, vec!["+"]);
let thread_pool = ThreadPool::new(1);
let mut result = test_me
.consume_line("\\ No newline at end of file", &thread_pool)
.unwrap();
assert_eq!(result.line_accepted, LineAcceptance::AcceptedDone);
assert_eq!(result.highlighted.len(), 1);
assert_eq!(
result.highlighted[0].get(),
"\u{1b}[32m+No trailing newline\u{1b}[0m\u{1b}[31m\u{1b}[7m⏎\u{1b}[0m\n\u{1b}[2m\\ No newline at end of file\u{1b}[0m\n",
);
}
}