Skip to content

Commit

Permalink
Move formatting to a separate struct
Browse files Browse the repository at this point in the history
  • Loading branch information
walles committed Oct 21, 2024
1 parent 01df0ae commit 315418c
Show file tree
Hide file tree
Showing 2 changed files with 126 additions and 120 deletions.
4 changes: 2 additions & 2 deletions src/plusminus_lines_highlighter.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use threadpool::ThreadPool;

use crate::lines_highlighter::{LineAcceptance, LinesHighlighter, Response};
use crate::refiner;
use crate::refiner::Formatter;
use crate::string_future::StringFuture;

#[derive(Debug)]
Expand Down Expand Up @@ -209,7 +209,7 @@ impl PlusMinusLinesHighlighter {
let return_me = StringFuture::from_function(
move || {
let mut result = String::new();
for line in refiner::format(
for line in Formatter::format(
&prefixes.iter().map(String::as_str).collect::<Vec<&str>>(),
&texts.iter().map(String::as_str).collect::<Vec<&str>>(),
) {
Expand Down
242 changes: 124 additions & 118 deletions src/refiner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,136 +5,142 @@ use crate::line_collector::NO_EOF_NEWLINE_MARKER_HOLDER;
use crate::token_collector::*;
use crate::tokenizer;

/// Format old and new lines in OLD and NEW colors.
///
/// No intra-line refinement.
#[must_use]
fn format_simple(prefixes: &[&str], prefix_texts: &[&str]) -> Vec<String> {
let mut lines: Vec<String> = Vec::new();

for (prefix, prefix_text) in prefixes.iter().zip(prefix_texts.iter()) {
let line_style = if prefix.contains('+') {
LINE_STYLE_NEW
} else {
LINE_STYLE_OLD
};

// If the user adds a section with a missing trailing newline, we want
// to draw a highlighted-in-red newline symbol at the end of the last
// line.
let draw_missing_trailing_newline = prefix.contains('+') && !prefix_text.ends_with('\n');

let last_pos = prefix_text.lines().count() - 1;
for (pos, line) in prefix_text.lines().enumerate() {
let last_line = pos == last_pos;

let to_push = render_row(
&line_style,
prefix,
&[StyledToken::new(
line.to_string(),
Style::DiffPartMidlighted,
)],
false,
);
if last_line && draw_missing_trailing_newline {
lines.push(format!("{to_push}{OLD}{INVERSE_VIDEO}⏎{NORMAL}"));
pub(crate) struct Formatter {}

impl Formatter {
/// Format old and new lines in OLD and NEW colors.
///
/// No intra-line refinement.
#[must_use]
fn format_simple(prefixes: &[&str], prefix_texts: &[&str]) -> Vec<String> {
let mut lines: Vec<String> = Vec::new();

for (prefix, prefix_text) in prefixes.iter().zip(prefix_texts.iter()) {
let line_style = if prefix.contains('+') {
LINE_STYLE_NEW
} else {
lines.push(to_push);
LINE_STYLE_OLD
};

// If the user adds a section with a missing trailing newline, we want
// to draw a highlighted-in-red newline symbol at the end of the last
// line.
let draw_missing_trailing_newline =
prefix.contains('+') && !prefix_text.ends_with('\n');

let last_pos = prefix_text.lines().count() - 1;
for (pos, line) in prefix_text.lines().enumerate() {
let last_line = pos == last_pos;

let to_push = render_row(
&line_style,
prefix,
&[StyledToken::new(
line.to_string(),
Style::DiffPartMidlighted,
)],
false,
);
if last_line && draw_missing_trailing_newline {
lines.push(format!("{to_push}{OLD}{INVERSE_VIDEO}⏎{NORMAL}"));
} else {
lines.push(to_push);
}
}
}

if !prefix_text.ends_with('\n') {
let no_eof_newline_marker_guard = NO_EOF_NEWLINE_MARKER_HOLDER.lock().unwrap();
let no_eof_newline_marker = no_eof_newline_marker_guard.as_ref().unwrap();
lines.push(format!(
"{NO_EOF_NEWLINE_COLOR}{no_eof_newline_marker}{NORMAL}"
));
if !prefix_text.ends_with('\n') {
let no_eof_newline_marker_guard = NO_EOF_NEWLINE_MARKER_HOLDER.lock().unwrap();
let no_eof_newline_marker = no_eof_newline_marker_guard.as_ref().unwrap();
lines.push(format!(
"{NO_EOF_NEWLINE_COLOR}{no_eof_newline_marker}{NORMAL}"
));
}
}
}

return lines;
}

/// LCS is O(m * n) complexity. If it gets too complex, refining will take too
/// much time and memory, so we shouldn't.
///
/// Ref: https://github.com/walles/riff/issues/35
fn too_large_to_refine(texts: &[&str]) -> bool {
let size = texts.iter().map(|text| text.len()).sum::<usize>();
return lines;
}

// Around this point refining starts taking near one second on Johan's
// laptop. Numbers have been invented through experimentation.
return size > 13_000usize * 13_000usize;
}
/// LCS is O(m * n) complexity. If it gets too complex, refining will take too
/// much time and memory, so we shouldn't.
///
/// Ref: https://github.com/walles/riff/issues/35
fn too_large_to_refine(texts: &[&str]) -> bool {
let size = texts.iter().map(|text| text.len()).sum::<usize>();

/// Returns a vector of ANSI highlighted lines.
///
/// `prefix_texts` are multi line strings. Having or not having trailing
/// newlines will affect tokenization. The lines are not expected to have any
/// prefixes like `+` or `-`.
///
/// `prefixes` are the prefixes to use for each `prefix_texts` text.
#[must_use]
pub fn format(prefixes: &[&str], prefix_texts: &[&str]) -> Vec<String> {
if prefixes.len() < 2 {
// Nothing to compare, we can't highlight anything
return format_simple(prefixes, prefix_texts);
}
if !prefixes.iter().any(|prefix| prefix.contains('+')) {
// Nothing added, we can't highlight anything
return format_simple(prefixes, prefix_texts);
// Around this point refining starts taking near one second on Johan's
// laptop. Numbers have been invented through experimentation.
return size > 13_000usize * 13_000usize;
}

if too_large_to_refine(prefix_texts) {
return format_simple(prefixes, prefix_texts);
}
/// Returns a vector of ANSI highlighted lines.
///
/// `prefix_texts` are multi line strings. Having or not having trailing
/// newlines will affect tokenization. The lines are not expected to have any
/// prefixes like `+` or `-`.
///
/// `prefixes` are the prefixes to use for each `prefix_texts` text.
#[must_use]
pub fn format(prefixes: &[&str], prefix_texts: &[&str]) -> Vec<String> {
if prefixes.len() < 2 {
// Nothing to compare, we can't highlight anything
return Self::format_simple(prefixes, prefix_texts);
}
if !prefixes.iter().any(|prefix| prefix.contains('+')) {
// Nothing added, we can't highlight anything
return Self::format_simple(prefixes, prefix_texts);
}

// This is what all old texts will be compared against
let new_text = prefix_texts.last().unwrap();
let new_prefix = prefixes.last().unwrap();
if Self::too_large_to_refine(prefix_texts) {
return Self::format_simple(prefixes, prefix_texts);
}

// These are all except for the last element
let old_prefixes = &prefixes[0..prefixes.len() - 1];
let old_prefix_texts = &prefix_texts[0..prefix_texts.len() - 1];
// This is what all old texts will be compared against
let new_text = prefix_texts.last().unwrap();
let new_prefix = prefixes.last().unwrap();

let mut old_tokens = vec![];
let mut new_tokens = vec![];
for old_text in old_prefix_texts.iter() {
let (old_tokens_internal, new_tokens_internal) = to_highlighted_tokens(old_text, new_text);
// These are all except for the last element
let old_prefixes = &prefixes[0..prefixes.len() - 1];
let old_prefix_texts = &prefix_texts[0..prefix_texts.len() - 1];

old_tokens.push(old_tokens_internal);
let mut old_tokens = vec![];
let mut new_tokens = vec![];
for old_text in old_prefix_texts.iter() {
let (old_tokens_internal, new_tokens_internal) =
to_highlighted_tokens(old_text, new_text);

if new_tokens.is_empty() {
// First iteration, just remember the new tokens
new_tokens = new_tokens_internal;
continue;
}
old_tokens.push(old_tokens_internal);

// Subsequent iterations, merge the new token styles
for (new_token, new_token_internal) in new_tokens.iter_mut().zip(new_tokens_internal) {
if new_token_internal.style as u8 > new_token.style as u8 {
new_token.style = new_token_internal.style;
if new_tokens.is_empty() {
// First iteration, just remember the new tokens
new_tokens = new_tokens_internal;
continue;
}

// Subsequent iterations, merge the new token styles
for (new_token, new_token_internal) in new_tokens.iter_mut().zip(new_tokens_internal) {
if new_token_internal.style as u8 > new_token.style as u8 {
new_token.style = new_token_internal.style;
}
}
}
}

// We should now have one token vector per old text
assert_eq!(old_tokens.len(), prefix_texts.len() - 1);
// We should now have one token vector per old text
assert_eq!(old_tokens.len(), prefix_texts.len() - 1);

// Now turn all our token vectors (all vectors in old_tokens plus
// new_tokens) into lines of highlighted text
// Now turn all our token vectors (all vectors in old_tokens plus
// new_tokens) into lines of highlighted text

// First render() into strings, then to_lines() into lines
let mut highlighted_lines = Vec::new();
for (prefix, tokens) in old_prefixes.iter().zip(old_tokens.iter()) {
let text = render(&LINE_STYLE_OLD, prefix, tokens);
highlighted_lines.extend(to_lines(&text));
}
let new_text = render(&LINE_STYLE_NEW, new_prefix, &new_tokens);
highlighted_lines.extend(to_lines(&new_text));
// First render() into strings, then to_lines() into lines
let mut highlighted_lines = Vec::new();
for (prefix, tokens) in old_prefixes.iter().zip(old_tokens.iter()) {
let text = render(&LINE_STYLE_OLD, prefix, tokens);
highlighted_lines.extend(to_lines(&text));
}
let new_text = render(&LINE_STYLE_NEW, new_prefix, &new_tokens);
highlighted_lines.extend(to_lines(&new_text));

return highlighted_lines;
return highlighted_lines;
}
}

fn should_highlight_change(tokens: &[&str], whitespace_only_is_fine: bool) -> bool {
Expand Down Expand Up @@ -327,15 +333,15 @@ mod tests {
#[test]
fn test_simple_format_adds_and_removes() {
let empty: Vec<String> = Vec::new();
assert_eq!(format_simple(&[], &[]), empty);
assert_eq!(Formatter::format_simple(&[], &[]), empty);

// Test adds-only
assert_eq!(
format_simple(&["+"], &["a\n"]),
Formatter::format_simple(&["+"], &["a\n"]),
["".to_string() + GREEN + "+a" + NORMAL]
);
assert_eq!(
format_simple(&["+"], &["a\nb\n"]),
Formatter::format_simple(&["+"], &["a\nb\n"]),
[
"".to_string() + GREEN + "+a" + NORMAL,
"".to_string() + GREEN + "+b" + NORMAL,
Expand All @@ -344,11 +350,11 @@ mod tests {

// Test removes-only
assert_eq!(
format_simple(&["-"], &["a\n"]),
Formatter::format_simple(&["-"], &["a\n"]),
["".to_string() + OLD + "-a" + NORMAL]
);
assert_eq!(
format_simple(&["-"], &["a\nb\n"]),
Formatter::format_simple(&["-"], &["a\nb\n"]),
[
"".to_string() + OLD + "-a" + NORMAL,
"".to_string() + OLD + "-b" + NORMAL,
Expand All @@ -373,13 +379,13 @@ mod tests {
let prefixes = vec!["+"];
let texts = vec![text.as_str()];

let result = format_simple(&prefixes, &texts);
let result = Formatter::format_simple(&prefixes, &texts);
assert_eq!(text.lines().count(), result.len());
}

#[test]
fn test_quote_change() {
let result = format(
let result = Formatter::format(
&["-", "+"],
&[
"<unchanged text between quotes>\n",
Expand All @@ -401,10 +407,10 @@ mod tests {

#[test]
fn test_almost_empty_changes() {
let result = format(&["-"], &["x\n"]);
let result = Formatter::format(&["-"], &["x\n"]);
assert_eq!(result, [format!("{OLD}-x{NORMAL}"),]);

let result = format(&["+"], &["x\n"]);
let result = Formatter::format(&["+"], &["x\n"]);
assert_eq!(result, [format!("{GREEN}+x{NORMAL}"),]);
}

Expand Down

0 comments on commit 315418c

Please sign in to comment.