Line up visible lines and include gaps if necessary

pull/25/head
Wilfred Hughes 2021-07-05 23:08:52 +07:00
parent 5463c120f4
commit da8a4c1ee1
5 changed files with 166 additions and 55 deletions

@ -1,5 +1,7 @@
## 0.3 (unreleased) ## 0.3 (unreleased)
Diffs are now displayed with unchanged lines aligned to the other side.
Improved Rust parsing to recognise lifetime syntax `'foo`, character Improved Rust parsing to recognise lifetime syntax `'foo`, character
literals `'x'` and sequences of punctuation. literals `'x'` and sequences of punctuation.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 72 KiB

@ -1,7 +1,8 @@
use crate::positions::SingleLineSpan; use crate::positions::SingleLineSpan;
use crate::syntax::{MatchKind, MatchedPos}; use crate::syntax::{aligned_lines, MatchKind, MatchedPos};
use regex::Regex; use regex::Regex;
use std::cmp::{max, min, Ordering}; use std::cmp::{max, min, Ordering};
use std::collections::HashMap;
use std::fmt; use std::fmt;
use std::ops::RangeInclusive; use std::ops::RangeInclusive;
@ -43,22 +44,30 @@ impl LineGroup {
} }
} }
// We can't iterate over a RangeInclusive<LineNumber> in safe, stable Rust. See fn lhs_lines(&self) -> Vec<LineNumber> {
// https://github.com/rust-lang/rust/issues/42168 let mut res = vec![];
pub fn iter_lhs_lines(&self) -> RangeInclusive<usize> {
let empty_iter = 1usize..=0;
match &self.lhs_lines { match &self.lhs_lines {
Some(lhs_lines) => lhs_lines.start().number..=lhs_lines.end().number, Some(lhs_lines) => {
None => empty_iter, for line in lhs_lines.start().number..=lhs_lines.end().number {
res.push(line.into());
}
}
None => {}
} }
res
} }
pub fn iter_rhs_lines(&self) -> RangeInclusive<usize> { fn rhs_lines(&self) -> Vec<LineNumber> {
let empty_iter = 1usize..=0; let mut res = vec![];
match &self.rhs_lines { match &self.rhs_lines {
Some(rhs_lines) => rhs_lines.start().number..=rhs_lines.end().number, Some(rhs_lines) => {
None => empty_iter, for line in rhs_lines.start().number..=rhs_lines.end().number {
res.push(line.into());
}
}
None => {}
} }
res
} }
pub fn pad(&mut self, amount: usize, max_lhs_line: LineNumber, max_rhs_line: LineNumber) { pub fn pad(&mut self, amount: usize, max_lhs_line: LineNumber, max_rhs_line: LineNumber) {
@ -393,25 +402,45 @@ fn apply_group(
lhs_lines: &[&str], lhs_lines: &[&str],
rhs_lines: &[&str], rhs_lines: &[&str],
group: &LineGroup, group: &LineGroup,
lhs_line_matches: &HashMap<LineNumber, LineNumber>,
lhs_content_width: usize, lhs_content_width: usize,
lhs_column_width: usize, lhs_column_width: usize,
rhs_column_width: usize, rhs_column_width: usize,
) -> String { ) -> String {
let mut lhs_result = String::new(); let mut lhs_result = String::new();
for lhs_line_num in group.iter_lhs_lines() { let mut rhs_result = String::new();
lhs_result.push_str(&format_line_num_padded(lhs_line_num, lhs_column_width));
match lhs_lines.get(lhs_line_num) { for (lhs_line_num, rhs_line_num) in
Some(line) => lhs_result.push_str(line), aligned_lines(&group.lhs_lines(), &group.rhs_lines(), lhs_line_matches)
None => lhs_result.push_str(&" ".repeat(lhs_content_width)), {
// TODO: we could build up a single string rather than
// horizontally concatenating afterwards.
match lhs_line_num {
Some(lhs_line_num) => {
lhs_result.push_str(&format_line_num_padded(
lhs_line_num.number,
lhs_column_width,
));
lhs_result.push_str(lhs_lines[lhs_line_num.number]);
}
None => {
lhs_result.push_str(&" ".repeat(lhs_column_width));
lhs_result.push_str(&" ".repeat(lhs_content_width));
}
} }
lhs_result.push('\n'); lhs_result.push('\n');
}
let mut rhs_result = String::new(); match rhs_line_num {
for rhs_line_num in group.iter_rhs_lines() { Some(rhs_line_num) => {
rhs_result.push_str(&format_line_num_padded(rhs_line_num, rhs_column_width)); rhs_result.push_str(&format_line_num_padded(
rhs_result.push_str(rhs_lines.get(rhs_line_num).unwrap_or(&"")); rhs_line_num.number,
rhs_column_width,
));
rhs_result.push_str(rhs_lines[rhs_line_num.number]);
}
None => {}
}
rhs_result.push('\n'); rhs_result.push('\n');
} }
@ -429,6 +458,7 @@ pub fn apply_groups(
lhs: &str, lhs: &str,
rhs: &str, rhs: &str,
groups: &[LineGroup], groups: &[LineGroup],
lhs_line_matches: &HashMap<LineNumber, LineNumber>,
lhs_content_width: usize, lhs_content_width: usize,
lhs_column_width: usize, lhs_column_width: usize,
rhs_column_width: usize, rhs_column_width: usize,
@ -445,6 +475,7 @@ pub fn apply_groups(
&lhs_lines, &lhs_lines,
&rhs_lines, &rhs_lines,
group, group,
lhs_line_matches,
lhs_content_width, lhs_content_width,
lhs_column_width, lhs_column_width,
rhs_column_width, rhs_column_width,

@ -16,7 +16,7 @@ use crate::lines::{
}; };
use crate::parse::{find_lang, parse, parse_lines, read_or_die, ConfigDir}; use crate::parse::{find_lang, parse, parse_lines, read_or_die, ConfigDir};
use crate::style::apply_colors; use crate::style::apply_colors;
use crate::syntax::{change_positions, set_next}; use crate::syntax::{change_positions, matching_lines, set_next};
fn term_width() -> Option<usize> { fn term_width() -> Option<usize> {
term_size::dimensions().map(|(w, _)| w) term_size::dimensions().map(|(w, _)| w)
@ -91,6 +91,8 @@ fn main() {
let lhs_positions = change_positions(&lhs_src, &rhs_src, &lhs); let lhs_positions = change_positions(&lhs_src, &rhs_src, &lhs);
let rhs_positions = change_positions(&rhs_src, &lhs_src, &rhs); let rhs_positions = change_positions(&rhs_src, &lhs_src, &rhs);
let lhs_matched_lines = matching_lines(&lhs);
let mut groups = visible_groups(&lhs_positions, &rhs_positions); let mut groups = visible_groups(&lhs_positions, &rhs_positions);
for group in &mut groups { for group in &mut groups {
group.pad(3, lhs_src.max_line(), rhs_src.max_line()); group.pad(3, lhs_src.max_line(), rhs_src.max_line());
@ -127,6 +129,7 @@ fn main() {
&lhs_colored, &lhs_colored,
&rhs_colored, &rhs_colored,
&groups, &groups,
&lhs_matched_lines,
lhs_content_width, lhs_content_width,
lhs_column_width, lhs_column_width,
rhs_column_width, rhs_column_width,

@ -3,8 +3,9 @@
use itertools::{EitherOrBoth, Itertools}; use itertools::{EitherOrBoth, Itertools};
use std::cell::Cell; use std::cell::Cell;
use std::cmp::min;
use std::collections::hash_map::DefaultHasher; use std::collections::hash_map::DefaultHasher;
use std::collections::HashMap; use std::collections::{HashMap, HashSet};
use std::fmt; use std::fmt;
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
use typed_arena::Arena; use typed_arena::Arena;
@ -520,6 +521,22 @@ fn change_positions_<'a>(
} }
} }
fn zip_pad_shorter<Tx: Copy, Ty: Copy>(lhs: &[Tx], rhs: &[Ty]) -> Vec<(Option<Tx>, Option<Ty>)> {
let mut res = vec![];
let mut i = 0;
loop {
match (lhs.get(i), rhs.get(i)) {
(None, None) => break,
(x, y) => res.push((x.map(|v| *v), y.map(|v| *v))),
}
i += 1;
}
res
}
/// Given two slices of line positions, return a list of line number /// Given two slices of line positions, return a list of line number
/// pairs. If the slices have different lengths, reuse the last item /// pairs. If the slices have different lengths, reuse the last item
/// from the shorter slice. /// from the shorter slice.
@ -555,59 +572,53 @@ pub fn aligned_lines(
lhs_lines: &[LineNumber], lhs_lines: &[LineNumber],
rhs_lines: &[LineNumber], rhs_lines: &[LineNumber],
lhs_line_matches: &HashMap<LineNumber, LineNumber>, lhs_line_matches: &HashMap<LineNumber, LineNumber>,
rhs_line_matches: &HashMap<LineNumber, LineNumber>,
) -> Vec<(Option<LineNumber>, Option<LineNumber>)> { ) -> Vec<(Option<LineNumber>, Option<LineNumber>)> {
// Find RHS lines that we can match up. let mut rhs_lines_available: HashSet<_> = rhs_lines.iter().collect();
let mut lhs_opposite_lines = vec![];
for lhs_line in lhs_lines {
if let Some(lhs_opposite_line) = lhs_line_matches.get(lhs_line) {
lhs_opposite_lines.push(lhs_opposite_line);
}
}
// Find LHS lines that we can match up. // For every LHS line, if there is a RHS line that included in
let mut rhs_opposite_lines = vec![]; // `rhs_lines` and hasn't yet been paired up, add it to matched_lines.
for rhs_line in rhs_lines { let mut matched_lines = vec![];
if let Some(rhs_opposite_line) = rhs_line_matches.get(rhs_line) { for lhs_line in lhs_lines {
rhs_opposite_lines.push(rhs_opposite_line); if let Some(rhs_line) = lhs_line_matches.get(lhs_line) {
if rhs_lines_available.remove(&rhs_line) {
matched_lines.push((lhs_line, rhs_line));
}
} }
} }
// Sanity check: if LHS X matches RHS Y, then RHS Y should match
// LHS X and we should have the same number of opposite lines.
assert_eq!(lhs_opposite_lines.len(), rhs_opposite_lines.len());
let mut res = vec![]; let mut res = vec![];
let mut lhs_i = 0; let mut lhs_i = 0;
let mut rhs_i = 0; let mut rhs_i = 0;
// Build a vec of matched lines, padding the unmatched sequences with None. // Build a vec of matched line tuples. For lines without matches
for (lhs_matched_line, rhs_matched_line) in rhs_opposite_lines // (novel lines, empty lines), just match lines up pairwise. Pad
.into_iter() // gaps if one side has more lines.
.zip(lhs_opposite_lines.into_iter()) for (lhs_matched_line, rhs_matched_line) in matched_lines {
{ let mut lhs_prev_lines = vec![];
while lhs_i < lhs_lines.len() && lhs_lines[lhs_i] < *lhs_matched_line { while lhs_i < lhs_lines.len() && lhs_lines[lhs_i] < *lhs_matched_line {
res.push((Some(lhs_lines[lhs_i]), None)); lhs_prev_lines.push(lhs_lines[lhs_i]);
lhs_i += 1; lhs_i += 1;
} }
let mut rhs_prev_lines = vec![];
while rhs_i < rhs_lines.len() && rhs_lines[rhs_i] < *rhs_matched_line { while rhs_i < rhs_lines.len() && rhs_lines[rhs_i] < *rhs_matched_line {
res.push((None, Some(rhs_lines[rhs_i]))); rhs_prev_lines.push(rhs_lines[rhs_i]);
rhs_i += 1; rhs_i += 1;
} }
res.push((Some(*lhs_matched_line), Some(*rhs_matched_line)));
}
// If we have trailing unmatched lines on other side, add them now. res.extend(zip_pad_shorter(&lhs_prev_lines, &rhs_prev_lines));
while lhs_i < lhs_lines.len() {
res.push((Some(lhs_lines[lhs_i]), None)); res.push((Some(*lhs_matched_line), Some(*rhs_matched_line)));
lhs_i += 1; lhs_i += 1;
}
while rhs_i < rhs_lines.len() {
res.push((None, Some(rhs_lines[rhs_i])));
rhs_i += 1; rhs_i += 1;
} }
// Handle unmatched lines after the last match.
res.extend(zip_pad_shorter(
&lhs_lines[min(lhs_i, lhs_lines.len())..],
&rhs_lines[min(rhs_i, rhs_lines.len())..],
));
res res
} }
@ -675,6 +686,70 @@ mod tests {
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use AtomKind::Other; use AtomKind::Other;
#[test]
fn test_aligned_middle() {
let lhs_lines: Vec<LineNumber> = vec![1.into(), 2.into()];
let rhs_lines: Vec<LineNumber> = vec![12.into(), 13.into()];
let mut line_matches: HashMap<LineNumber, LineNumber> = HashMap::new();
line_matches.insert(2.into(), 12.into());
assert_eq!(
aligned_lines(&lhs_lines, &rhs_lines, &line_matches),
vec![
(Some(1.into()), None),
(Some(2.into()), Some(12.into())),
(None, Some(13.into()))
]
);
}
#[test]
fn test_aligned_all() {
let lhs_lines: Vec<LineNumber> = vec![1.into(), 2.into()];
let rhs_lines: Vec<LineNumber> = vec![11.into(), 12.into()];
let mut line_matches: HashMap<LineNumber, LineNumber> = HashMap::new();
line_matches.insert(1.into(), 2.into());
line_matches.insert(2.into(), 12.into());
assert_eq!(
aligned_lines(&lhs_lines, &rhs_lines, &line_matches),
vec![
(Some(1.into()), Some(11.into())),
(Some(2.into()), Some(12.into())),
]
);
}
#[test]
fn test_aligned_none() {
let lhs_lines: Vec<LineNumber> = vec![1.into()];
let rhs_lines: Vec<LineNumber> = vec![11.into()];
let line_matches: HashMap<LineNumber, LineNumber> = HashMap::new();
assert_eq!(
aligned_lines(&lhs_lines, &rhs_lines, &line_matches),
vec![(Some(1.into()), Some(11.into()))]
);
}
#[test]
fn test_aligned_line_overlap() {
let lhs_lines: Vec<LineNumber> = vec![1.into(), 2.into()];
let rhs_lines: Vec<LineNumber> = vec![11.into()];
let mut line_matches: HashMap<LineNumber, LineNumber> = HashMap::new();
line_matches.insert(1.into(), 11.into());
line_matches.insert(2.into(), 11.into());
assert_eq!(
aligned_lines(&lhs_lines, &rhs_lines, &line_matches),
vec![(Some(1.into()), Some(11.into())), (Some(2.into()), None)]
);
}
/// Ensure that we assign prev_opposite_pos even if the change is on the first node. /// Ensure that we assign prev_opposite_pos even if the change is on the first node.
#[test] #[test]
fn test_prev_opposite_pos_first_node() { fn test_prev_opposite_pos_first_node() {