├── .github ├── PULL_REQUEST_TEMPLATE.md ├── rust.json └── workflows │ ├── release.yml │ └── rust.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── README.md ├── screens ├── app.png ├── diff_chars.png ├── diff_lines.png └── diff_slice.png └── src ├── basic.rs ├── format_table.rs ├── lcs.rs ├── lib.rs └── text.rs /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | # TODO (check if already done) 3 | * [ ] Add tests 4 | * [ ] Add CHANGELOG.md entry 5 | -------------------------------------------------------------------------------- /.github/rust.json: -------------------------------------------------------------------------------- 1 | { 2 | "problemMatcher": [ 3 | { 4 | "owner": "cargo-common", 5 | "pattern": [ 6 | { 7 | "regexp": "^(warning|warn|error)(\\[(\\S*)\\])?: (.*)$", 8 | "severity": 1, 9 | "message": 4, 10 | "code": 3 11 | }, 12 | { 13 | "regexp": "^\\s+-->\\s(\\S+):(\\d+):(\\d+)$", 14 | "file": 1, 15 | "line": 2, 16 | "column": 3 17 | } 18 | ] 19 | }, 20 | { 21 | "owner": "cargo-test", 22 | "pattern": [ 23 | { 24 | "regexp": "^.*panicked\\s+at\\s+'(.*)',\\s+(.*):(\\d+):(\\d+)$", 25 | "message": 1, 26 | "file": 2, 27 | "line": 3, 28 | "column": 4 29 | } 30 | ] 31 | }, 32 | { 33 | "owner": "cargo-fmt", 34 | "pattern": [ 35 | { 36 | "regexp": "^(Diff in (\\S+)) at line (\\d+):", 37 | "message": 1, 38 | "file": 2, 39 | "line": 3 40 | } 41 | ] 42 | } 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release new version 2 | 3 | on: 4 | workflow_dispatch: 5 | secrets: 6 | CARGO_REGISTRY_TOKEN: 7 | required: true 8 | 9 | env: 10 | RUST_BACKTRACE: 1 11 | CARGO_TERM_COLOR: always 12 | 13 | jobs: 14 | create-release: 15 | name: Create release 16 | runs-on: ubuntu-latest 17 | if: github.ref == 'refs/heads/main' 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | with: 22 | persist-credentials: true 23 | 24 | - name: Install rust 25 | uses: actions-rs/toolchain@v1 26 | with: 27 | toolchain: stable 28 | profile: minimal 29 | override: true 30 | 31 | - uses: Swatinem/rust-cache@v2 32 | 33 | # Determine which version we're about to publish, so we can tag it appropriately. 34 | # If the tag already exists, then we've already published this version. 35 | - name: Determine current version 36 | id: version-check 37 | run: | 38 | # Fail on first error, on undefined variables, and on errors in pipes. 39 | set -euo pipefail 40 | export VERSION="$(cargo metadata --format-version 1 | \ 41 | jq --arg crate_name prettydiff --exit-status -r \ 42 | '.packages[] | select(.name == $crate_name) | .version')" 43 | echo "version=$VERSION" >> $GITHUB_OUTPUT 44 | if [[ "$(git tag -l "$VERSION")" != '' ]]; then 45 | echo "Aborting: Version $VERSION is already published, we found its tag in the repo." 46 | exit 1 47 | fi 48 | 49 | # TODO: Replace this with the cargo-semver-checks v2 GitHub Action when it's stabilized: 50 | # https://github.com/obi1kenobi/cargo-semver-checks-action/pull/21 51 | - name: Semver-check 52 | run: | 53 | # Fail on first error, on undefined variables, and on errors in pipes. 54 | set -euo pipefail 55 | cargo install --locked cargo-semver-checks 56 | cargo semver-checks check-release 57 | 58 | - name: Publish 59 | run: cargo publish 60 | env: 61 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 62 | 63 | - name: Tag the version 64 | run: | 65 | # Fail on first error, on undefined variables, and on errors in pipes. 66 | set -euo pipefail 67 | git tag "${{ steps.version-check.outputs.version }}" 68 | git push origin "${{ steps.version-check.outputs.version }}" 69 | 70 | - uses: taiki-e/create-gh-release-action@v1 71 | name: Create GitHub release 72 | with: 73 | branch: main 74 | ref: refs/tags/${{ steps.version-check.outputs.version }} 75 | env: 76 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 77 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | fmt: 14 | name: check rustfmt 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: dtolnay/rust-toolchain@stable 19 | with: 20 | components: rustfmt 21 | - name: Format 22 | run: cargo fmt --check 23 | 24 | clippy: 25 | name: check clippy 26 | runs-on: ubuntu-latest 27 | steps: 28 | - uses: actions/checkout@v4 29 | - uses: dtolnay/rust-toolchain@stable 30 | with: 31 | components: clippy 32 | - name: Clippy 33 | run: cargo clippy -- -D warnings 34 | 35 | tests: 36 | strategy: 37 | matrix: 38 | include: 39 | - os: ubuntu-latest 40 | host_target: x86_64-unknown-linux-gnu 41 | - os: macos-latest 42 | host_target: x86_64-apple-darwin 43 | - os: windows-latest 44 | host_target: i686-pc-windows-msvc 45 | runs-on: ${{ matrix.os }} 46 | steps: 47 | - uses: actions/checkout@v4 48 | - uses: dtolnay/rust-toolchain@stable 49 | - name: Build 50 | run: cargo build --verbose 51 | - name: Run unit tests 52 | run: cargo test --verbose 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ### Added 11 | 12 | - Reduced build bloat. 13 | 14 | ### Fixed 15 | 16 | ### Changed 17 | 18 | ### Removed 19 | 20 | ## 0.7.1 21 | 22 | ### Changed 23 | 24 | - Updated `owo-colors` to 4.0 25 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "prettydiff" 3 | version = "0.8.0" 4 | authors = ["Roman Koblov "] 5 | edition = "2018" 6 | description = "Side-by-side diff for two files" 7 | categories = ["text-processing"] 8 | keywords = ["diff", "text", "compare", "changes"] 9 | license = "MIT" 10 | readme = "README.md" 11 | repository = "https://github.com/romankoblov/prettydiff" 12 | homepage = "https://github.com/romankoblov/prettydiff" 13 | rust-version = "1.70" 14 | 15 | [dependencies] 16 | owo-colors = "4.0" 17 | pad = "0.1.6" 18 | prettytable-rs = { version = "0.10.0", optional = true, default-features = false } 19 | 20 | [features] 21 | cli = ["prettytable-rs"] 22 | default = ["cli"] 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Roman Koblov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # prettydiff 2 | 3 | [![Crate](https://img.shields.io/crates/v/prettydiff.svg)](https://crates.io/crates/prettydiff) 4 | [![docs.rs](https://docs.rs/prettydiff/badge.svg)](https://docs.rs/prettydiff) 5 | 6 | Side-by-side diff for two files in Rust. App && Library. 7 | 8 | ## Examples 9 | 10 | ### Slice diff 11 | 12 | ```rust 13 | use prettydiff::diff_slice; 14 | 15 | println!("Diff: {}", diff_slice(&[1, 2, 3, 4, 5, 6], &[2, 3, 5, 7])); 16 | println!( 17 | "Diff: {}", 18 | diff_slice(&["q", "a", "b", "x", "c", "d"], &["a", "b", "y", "c", "d", "f"]) 19 | ); 20 | println!( 21 | "Diff: {}", 22 | diff_slice(&["a", "c", "d", "b"], &["a", "e", "b"]) 23 | ); 24 | ``` 25 | 26 | ![diff_slice](https://raw.githubusercontent.com/romankoblov/prettydiff/master/screens/diff_slice.png) 27 | 28 | Get vector of changes: 29 | 30 | ```rust 31 | use prettydiff::diff_slice; 32 | 33 | assert_eq!( 34 | diff_slice(&["q", "a", "b", "x", "c", "d"], &["a", "b", "y", "c", "d", "f"]).diff, 35 | vec![ 36 | DiffOp::Remove(&["q"]), 37 | DiffOp::Equal(&["a", "b"]), 38 | DiffOp::Replace(&["x"], &["y"]), 39 | DiffOp::Equal(&["c", "d"]), 40 | DiffOp::Insert(&["f"]), 41 | ] 42 | ); 43 | ``` 44 | 45 | ### Diff line by chars or words 46 | 47 | ![diff_chars](https://raw.githubusercontent.com/romankoblov/prettydiff/master/screens/diff_chars.png) 48 | 49 | ```rust 50 | use prettydiff::{diff_chars, diff_words}; 51 | 52 | println!("diff_chars: {}", diff_chars("abefcd", "zadqwc")); 53 | println!( 54 | "diff_chars: {}", 55 | diff_chars( 56 | "The quick brown fox jumps over the lazy dog", 57 | "The quick brown dog leaps over the lazy cat" 58 | ) 59 | ); 60 | println!( 61 | "diff_chars: {}", 62 | diff_chars( 63 | "The red brown fox jumped over the rolling log", 64 | "The brown spotted fox leaped over the rolling log" 65 | ) 66 | ); 67 | println!( 68 | "diff_chars: {}", 69 | diff_chars( 70 | "The red brown fox jumped over the rolling log", 71 | "The brown spotted fox leaped over the rolling log" 72 | ) 73 | .set_highlight_whitespace(true) 74 | ); 75 | println!( 76 | "diff_words: {}", 77 | diff_words( 78 | "The red brown fox jumped over the rolling log", 79 | "The brown spotted fox leaped over the rolling log" 80 | ) 81 | ); 82 | println!( 83 | "diff_words: {}", 84 | diff_words( 85 | "The quick brown fox jumps over the lazy dog", 86 | "The quick, brown dog leaps over the lazy cat" 87 | ) 88 | ); 89 | ``` 90 | 91 | ### Diff lines 92 | 93 | ![diff_lines](https://raw.githubusercontent.com/romankoblov/prettydiff/master/screens/diff_lines.png) 94 | 95 | ```rust 96 | use prettydiff::diff_lines; 97 | 98 | let code1_a = r#" 99 | void func1() { 100 | x += 1 101 | } 102 | 103 | void func2() { 104 | x += 2 105 | } 106 | "#; 107 | let code1_b = r#" 108 | void func1(a: u32) { 109 | x += 1 110 | } 111 | 112 | void functhreehalves() { 113 | x += 1.5 114 | } 115 | 116 | void func2() { 117 | x += 2 118 | } 119 | 120 | void func3(){} 121 | "#; 122 | println!("diff_lines:"); 123 | println!("{}", diff_lines(code1_a, code1_b)); 124 | ``` 125 | -------------------------------------------------------------------------------- /screens/app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oli-obk/prettydiff/4fe96e77456a69cec1f99099646250b5e4b58526/screens/app.png -------------------------------------------------------------------------------- /screens/diff_chars.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oli-obk/prettydiff/4fe96e77456a69cec1f99099646250b5e4b58526/screens/diff_chars.png -------------------------------------------------------------------------------- /screens/diff_lines.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oli-obk/prettydiff/4fe96e77456a69cec1f99099646250b5e4b58526/screens/diff_lines.png -------------------------------------------------------------------------------- /screens/diff_slice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oli-obk/prettydiff/4fe96e77456a69cec1f99099646250b5e4b58526/screens/diff_slice.png -------------------------------------------------------------------------------- /src/basic.rs: -------------------------------------------------------------------------------- 1 | //! Basic diff functions 2 | use crate::lcs; 3 | use owo_colors::OwoColorize; 4 | use std::fmt; 5 | 6 | /// Single change in original slice needed to get new slice 7 | #[derive(Debug, PartialEq, Eq)] 8 | pub enum DiffOp<'a, T: 'a> { 9 | /// Appears only in second slice 10 | Insert(&'a [T]), 11 | /// Appears in both slices, but changed 12 | Replace(&'a [T], &'a [T]), 13 | /// Appears only in first slice 14 | Remove(&'a [T]), 15 | /// Appears on both slices 16 | Equal(&'a [T]), 17 | } 18 | 19 | /// Diffs any slices which implements PartialEq 20 | pub fn diff<'a, T: PartialEq>(x: &'a [T], y: &'a [T]) -> Vec> { 21 | let mut ops: Vec> = Vec::new(); 22 | let table = lcs::Table::new(x, y); 23 | 24 | let mut i = 0; 25 | let mut j = 0; 26 | 27 | for m in table.matches_zero() { 28 | let x_seq = &x[i..m.x]; 29 | let y_seq = &y[j..m.y]; 30 | 31 | if i < m.x && j < m.y { 32 | ops.push(DiffOp::Replace(x_seq, y_seq)); 33 | } else if i < m.x { 34 | ops.push(DiffOp::Remove(x_seq)); 35 | } else if j < m.y { 36 | ops.push(DiffOp::Insert(y_seq)); 37 | } 38 | 39 | i = m.x + m.len; 40 | j = m.y + m.len; 41 | 42 | if m.len > 0 { 43 | ops.push(DiffOp::Equal(&x[m.x..i])); 44 | } 45 | } 46 | ops 47 | } 48 | 49 | /// Container for slice diff result. Can be pretty-printed by Display trait. 50 | #[derive(Debug, PartialEq, Eq)] 51 | pub struct SliceChangeset<'a, T> { 52 | pub diff: Vec>, 53 | } 54 | 55 | impl SliceChangeset<'_, T> { 56 | pub fn format(&self, skip_same: bool) -> String { 57 | let mut out: Vec = Vec::with_capacity(self.diff.len()); 58 | for op in &self.diff { 59 | match op { 60 | DiffOp::Equal(a) => { 61 | if !skip_same || a.len() == 1 { 62 | for i in a.iter() { 63 | out.push(format!(" {}", i)) 64 | } 65 | } else if a.len() > 1 { 66 | out.push(format!(" ... skip({}) ...", a.len())); 67 | } 68 | } 69 | 70 | DiffOp::Insert(a) => { 71 | for i in a.iter() { 72 | out.push((format!("+ {}", i).green()).to_string()); 73 | } 74 | } 75 | 76 | DiffOp::Remove(a) => { 77 | for i in a.iter() { 78 | out.push(format!("- {}", i).red().to_string()); 79 | } 80 | } 81 | DiffOp::Replace(a, b) => { 82 | let min_len = std::cmp::min(a.len(), b.len()); 83 | let max_len = std::cmp::max(a.len(), b.len()); 84 | 85 | for i in 0..min_len { 86 | out.push(format!("~ {} -> {}", a[i], b[i]).yellow().to_string()); 87 | } 88 | for i in min_len..max_len { 89 | if max_len == a.len() { 90 | out.push(format!("- {}", a[i]).red().to_string()); 91 | } else { 92 | out.push(format!("+ {}", b[i]).green().to_string()); 93 | } 94 | } 95 | } 96 | } 97 | } 98 | format!("[\n{}\n]", out.join(",\n")) 99 | } 100 | } 101 | 102 | impl fmt::Display for SliceChangeset<'_, T> { 103 | fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 104 | write!(formatter, "{}", self.format(true)) 105 | } 106 | } 107 | 108 | /// Diff two arbitary slices with elements that support Display trait 109 | pub fn diff_slice<'a, T: PartialEq + std::fmt::Display>( 110 | x: &'a [T], 111 | y: &'a [T], 112 | ) -> SliceChangeset<'a, T> { 113 | let diff = diff(x, y); 114 | SliceChangeset { diff } 115 | } 116 | 117 | #[test] 118 | fn test_basic() { 119 | assert_eq!( 120 | diff(&[1, 2, 3, 4, 5, 6], &[2, 3, 5, 7]), 121 | vec![ 122 | DiffOp::Remove(&[1]), 123 | DiffOp::Equal(&[2, 3]), 124 | DiffOp::Remove(&[4]), 125 | DiffOp::Equal(&[5]), 126 | DiffOp::Replace(&[6], &[7]), 127 | ] 128 | ); 129 | 130 | assert_eq!( 131 | diff_slice( 132 | &["q", "a", "b", "x", "c", "d"], 133 | &["a", "b", "y", "c", "d", "f"], 134 | ) 135 | .diff, 136 | vec![ 137 | DiffOp::Remove(&["q"]), 138 | DiffOp::Equal(&["a", "b"]), 139 | DiffOp::Replace(&["x"], &["y"]), 140 | DiffOp::Equal(&["c", "d"]), 141 | DiffOp::Insert(&["f"]), 142 | ] 143 | ); 144 | 145 | assert_eq!( 146 | diff(&["a", "c", "d", "b"], &["a", "e", "b"]), 147 | vec![ 148 | DiffOp::Equal(&["a"]), 149 | DiffOp::Replace(&["c", "d"], &["e"]), 150 | DiffOp::Equal(&["b"]), 151 | ] 152 | ); 153 | println!("Diff: {}", diff_slice(&[1, 2, 3, 4, 5, 6], &[2, 3, 5, 7])); 154 | println!( 155 | "Diff: {}", 156 | diff_slice( 157 | &["q", "a", "b", "x", "c", "d"], 158 | &["a", "b", "y", "c", "d", "f"] 159 | ) 160 | ); 161 | println!( 162 | "Diff: {}", 163 | diff_slice(&["a", "c", "d", "b"], &["a", "e", "b"]) 164 | ); 165 | } 166 | -------------------------------------------------------------------------------- /src/format_table.rs: -------------------------------------------------------------------------------- 1 | //! Setup unicode-formatted table for prettytable 2 | //! 3 | //! TODO: Move to separate crate 4 | 5 | use prettytable::format; 6 | use prettytable::Table; 7 | 8 | fn format_table(table: &mut Table) { 9 | table.set_format( 10 | format::FormatBuilder::new() 11 | .column_separator('│') 12 | .borders('│') 13 | .separators( 14 | &[format::LinePosition::Top], 15 | format::LineSeparator::new('─', '┬', '┌', '┐'), 16 | ) 17 | .separators( 18 | &[format::LinePosition::Title], 19 | format::LineSeparator::new('─', '┼', '├', '┤'), 20 | ) 21 | .separators( 22 | &[format::LinePosition::Intern], 23 | format::LineSeparator::new('─', '┼', '├', '┤'), 24 | ) 25 | .separators( 26 | &[format::LinePosition::Bottom], 27 | format::LineSeparator::new('─', '┴', '└', '┘'), 28 | ) 29 | .padding(1, 1) 30 | .build(), 31 | ); 32 | } 33 | /// Returns Table with unicode formatter 34 | pub fn new() -> Table { 35 | let mut table = Table::new(); 36 | format_table(&mut table); 37 | table 38 | } 39 | -------------------------------------------------------------------------------- /src/lcs.rs: -------------------------------------------------------------------------------- 1 | //! Common functions for [Longest common subsequences](https://en.wikipedia.org/wiki/Longest_common_subsequence_problem) 2 | //! on slice. 3 | 4 | cfg_prettytable! { 5 | use crate::format_table; 6 | use prettytable::{Cell, Row}; 7 | } 8 | use std::cmp::max; 9 | 10 | #[derive(Debug)] 11 | pub struct Table<'a, T: 'a> { 12 | x: &'a [T], 13 | y: &'a [T], 14 | table: Vec>, 15 | } 16 | 17 | /// Implements Longest Common Subsequences Table 18 | /// Memory requirement: O(N^2) 19 | /// 20 | /// Based on [Wikipedia article](https://en.wikipedia.org/wiki/Longest_common_subsequence_problem) 21 | impl<'a, T> Table<'a, T> 22 | where 23 | T: PartialEq, 24 | { 25 | /// Creates new table for search common subsequences in x and y 26 | pub fn new(x: &'a [T], y: &'a [T]) -> Table<'a, T> { 27 | let x_len = x.len() + 1; 28 | let y_len = y.len() + 1; 29 | let mut table = vec![vec![0; y_len]; x_len]; 30 | 31 | for i in 1..x_len { 32 | for j in 1..y_len { 33 | table[i][j] = if x[i - 1] == y[j - 1] { 34 | table[i - 1][j - 1] + 1 35 | } else { 36 | max(table[i][j - 1], table[i - 1][j]) 37 | }; 38 | } 39 | } 40 | 41 | Table { x, y, table } 42 | } 43 | 44 | fn seq_iter(&self) -> TableIter { 45 | TableIter { 46 | x: self.x.len(), 47 | y: self.y.len(), 48 | table: self, 49 | } 50 | } 51 | fn get_match(&self, x: usize, y: usize, len: usize) -> Match { 52 | Match { 53 | x, 54 | y, 55 | len, 56 | table: self, 57 | } 58 | } 59 | 60 | /// Returns matches between X and Y 61 | pub fn matches(&self) -> Vec> { 62 | let mut matches: Vec> = Vec::new(); 63 | for (x, y) in self.seq_iter() { 64 | if let Some(last) = matches.last_mut() { 65 | if last.x == x + 1 && last.y == y + 1 { 66 | last.x = x; 67 | last.y = y; 68 | last.len += 1; 69 | continue; 70 | } 71 | } 72 | matches.push(self.get_match(x, y, 1)); 73 | } 74 | matches.reverse(); 75 | matches 76 | } 77 | 78 | /// Returns matches between X and Y with zero-len match at the end 79 | pub fn matches_zero(&self) -> Vec> { 80 | let mut matches = self.matches(); 81 | matches.push(self.get_match(self.x.len(), self.y.len(), 0)); 82 | matches 83 | } 84 | 85 | /// Find longest sequence 86 | pub fn longest_seq(&self) -> Vec<&T> { 87 | self.matches(); 88 | let mut common: Vec<_> = self.seq_iter().map(|(x, _y)| &self.x[x]).collect(); 89 | common.reverse(); 90 | common 91 | } 92 | } 93 | 94 | #[cfg(feature = "prettytable-rs")] 95 | /// Prints pretty-table for LCS 96 | impl std::fmt::Display for Table<'_, T> 97 | where 98 | T: std::fmt::Display, 99 | { 100 | fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 101 | let mut table = format_table::new(); 102 | let mut header = vec!["".to_string(), "Ø".to_string()]; 103 | for i in self.x { 104 | header.push(format!("{}", i)); 105 | } 106 | 107 | table.set_titles(Row::new( 108 | header.into_iter().map(|i| Cell::new(&i)).collect(), 109 | )); 110 | for j in 0..=self.y.len() { 111 | let mut row = vec![if j == 0 { 112 | "Ø".to_string() 113 | } else { 114 | format!("{}", self.y[j - 1]) 115 | }]; 116 | for i in 0..=self.x.len() { 117 | row.push(format!("{}", self.table[i][j])); 118 | } 119 | table.add_row(row.into_iter().map(|i| Cell::new(&i)).collect()); 120 | } 121 | write!(formatter, "\n{}", table) 122 | } 123 | } 124 | 125 | struct TableIter<'a, T: 'a> { 126 | x: usize, 127 | y: usize, 128 | table: &'a Table<'a, T>, 129 | } 130 | 131 | impl Iterator for TableIter<'_, T> { 132 | type Item = (usize, usize); 133 | fn next(&mut self) -> Option { 134 | let table = &self.table.table; 135 | 136 | while self.x != 0 && self.y != 0 { 137 | let cur = table[self.x][self.y]; 138 | 139 | if cur == table[self.x - 1][self.y] { 140 | self.x -= 1; 141 | continue; 142 | } 143 | self.y -= 1; 144 | if cur == table[self.x][self.y] { 145 | continue; 146 | } 147 | self.x -= 1; 148 | return Some((self.x, self.y)); 149 | } 150 | None 151 | } 152 | } 153 | 154 | pub struct Match<'a, T: 'a> { 155 | pub x: usize, 156 | pub y: usize, 157 | pub len: usize, 158 | table: &'a Table<'a, T>, 159 | } 160 | 161 | impl Match<'_, T> { 162 | /// Returns matched sequence 163 | pub fn seq(&self) -> &[T] { 164 | &self.table.x[self.x..(self.x + self.len)] 165 | } 166 | } 167 | 168 | #[test] 169 | fn test_table() { 170 | let x = vec!["A", "G", "C", "A", "T"]; 171 | let y = vec!["G", "A", "C"]; 172 | 173 | let table = Table::new(&x, &y); 174 | assert_eq!( 175 | format!("{}", table).replace('\r', ""), 176 | r#" 177 | ┌───┬───┬───┬───┬───┬───┬───┐ 178 | │ │ Ø │ A │ G │ C │ A │ T │ 179 | ├───┼───┼───┼───┼───┼───┼───┤ 180 | │ Ø │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 181 | ├───┼───┼───┼───┼───┼───┼───┤ 182 | │ G │ 0 │ 0 │ 1 │ 1 │ 1 │ 1 │ 183 | ├───┼───┼───┼───┼───┼───┼───┤ 184 | │ A │ 0 │ 1 │ 1 │ 1 │ 2 │ 2 │ 185 | ├───┼───┼───┼───┼───┼───┼───┤ 186 | │ C │ 0 │ 1 │ 1 │ 2 │ 2 │ 2 │ 187 | └───┴───┴───┴───┴───┴───┴───┘ 188 | "# 189 | ); 190 | assert_eq!(table.longest_seq(), vec![&"A", &"C"]); 191 | } 192 | 193 | #[test] 194 | 195 | fn test_table_match() { 196 | let test_v = vec![ 197 | ( 198 | "The quick brown fox jumps over the lazy dog", 199 | "The quick brown dog leaps over the lazy cat", 200 | "The quick brown o ps over the lazy ", 201 | vec!["The quick brown ", "o", " ", "ps over the lazy "], 202 | ), 203 | ("ab:c", "ba:b:c", "ab:c", vec!["a", "b:c"]), 204 | ( 205 | "The red brown fox jumped over the rolling log", 206 | "The brown spotted fox leaped over the rolling log", 207 | "The brown fox ped over the rolling log", 208 | vec!["The ", "brown ", "fox ", "ped over the rolling log"], 209 | ), 210 | ]; 211 | for (x_str, y_str, exp_str, match_exp) in test_v { 212 | let x: Vec<_> = x_str.split("").collect(); 213 | let y: Vec<_> = y_str.split("").collect(); 214 | 215 | // Trim empty elements 216 | let table = Table::new(&x[1..(x.len() - 1)], &y[1..(y.len() - 1)]); 217 | let seq = table 218 | .longest_seq() 219 | .iter() 220 | .map(|i| i.to_string()) 221 | .collect::>() 222 | .join(""); 223 | assert_eq!(seq, exp_str); 224 | let matches: Vec<_> = table.matches().iter().map(|m| m.seq().join("")).collect(); 225 | assert_eq!(matches, match_exp); 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | macro_rules! cfg_prettytable {( $($item:item)* ) => ( 2 | $( 3 | #[cfg(feature = "prettytable-rs")] 4 | $item 5 | )* 6 | )} 7 | 8 | #[cfg(feature = "prettytable-rs")] 9 | #[macro_use] 10 | extern crate prettytable; 11 | 12 | pub mod basic; 13 | cfg_prettytable! { 14 | pub mod format_table; 15 | } 16 | pub mod lcs; 17 | pub mod text; 18 | 19 | pub use crate::basic::diff_slice; 20 | pub use crate::text::{diff_chars, diff_lines, diff_words}; 21 | pub use owo_colors; 22 | -------------------------------------------------------------------------------- /src/text.rs: -------------------------------------------------------------------------------- 1 | //! Utils for diff text 2 | use owo_colors::AnsiColors::{Green, Red}; 3 | use owo_colors::{AnsiColors, OwoColorize, Style}; 4 | 5 | use crate::basic; 6 | cfg_prettytable! { 7 | use crate::format_table; 8 | use prettytable::{Cell, Row}; 9 | } 10 | use std::{ 11 | cmp::{max, min}, 12 | fmt, 13 | }; 14 | 15 | use pad::{Alignment, PadStr}; 16 | 17 | pub struct StringSplitIter<'a, F> 18 | where 19 | F: Fn(char) -> bool, 20 | { 21 | last: usize, 22 | text: &'a str, 23 | matched: Option<&'a str>, 24 | iter: std::str::MatchIndices<'a, F>, 25 | } 26 | 27 | impl<'a, F> Iterator for StringSplitIter<'a, F> 28 | where 29 | F: Fn(char) -> bool, 30 | { 31 | type Item = &'a str; 32 | fn next(&mut self) -> Option { 33 | if let Some(m) = self.matched { 34 | self.matched = None; 35 | Some(m) 36 | } else if let Some((idx, matched)) = self.iter.next() { 37 | let res = if self.last != idx { 38 | self.matched = Some(matched); 39 | &self.text[self.last..idx] 40 | } else { 41 | matched 42 | }; 43 | self.last = idx + matched.len(); 44 | Some(res) 45 | } else if self.last < self.text.len() { 46 | let res = &self.text[self.last..]; 47 | self.last = self.text.len(); 48 | Some(res) 49 | } else { 50 | None 51 | } 52 | } 53 | } 54 | 55 | pub fn collect_strings(it: impl Iterator) -> Vec { 56 | it.map(|s| s.to_string()).collect::>() 57 | } 58 | 59 | /// Split string by clousure (Fn(char)->bool) keeping delemiters 60 | pub fn split_by_char_fn(text: &'_ str, pat: F) -> StringSplitIter<'_, F> 61 | where 62 | F: Fn(char) -> bool, 63 | { 64 | StringSplitIter { 65 | last: 0, 66 | text, 67 | matched: None, 68 | iter: text.match_indices(pat), 69 | } 70 | } 71 | 72 | /// Split string by non-alphanumeric characters keeping delemiters 73 | pub fn split_words(text: &str) -> impl Iterator { 74 | split_by_char_fn(text, |c: char| !c.is_alphanumeric()) 75 | } 76 | 77 | /// Container for inline text diff result. Can be pretty-printed by Display trait. 78 | #[derive(Debug, PartialEq)] 79 | pub struct InlineChangeset<'a> { 80 | old: Vec<&'a str>, 81 | new: Vec<&'a str>, 82 | separator: &'a str, 83 | highlight_whitespace: bool, 84 | insert_style: Style, 85 | insert_whitespace_style: Style, 86 | remove_style: Style, 87 | remove_whitespace_style: Style, 88 | } 89 | 90 | impl<'a> InlineChangeset<'a> { 91 | pub fn new(old: Vec<&'a str>, new: Vec<&'a str>) -> InlineChangeset<'a> { 92 | InlineChangeset { 93 | old, 94 | new, 95 | separator: "", 96 | highlight_whitespace: true, 97 | insert_style: Style::new().green(), 98 | insert_whitespace_style: Style::new().white().on_green(), 99 | remove_style: Style::new().red().strikethrough(), 100 | remove_whitespace_style: Style::new().white().on_red(), 101 | } 102 | } 103 | /// Highlight whitespaces in case of insert/remove? 104 | pub fn set_highlight_whitespace(mut self, val: bool) -> Self { 105 | self.highlight_whitespace = val; 106 | self 107 | } 108 | 109 | /// Style of inserted text 110 | pub fn set_insert_style(mut self, val: Style) -> Self { 111 | self.insert_style = val; 112 | self 113 | } 114 | 115 | /// Style of inserted whitespace 116 | pub fn set_insert_whitespace_style(mut self, val: Style) -> Self { 117 | self.insert_whitespace_style = val; 118 | self 119 | } 120 | 121 | /// Style of removed text 122 | pub fn set_remove_style(mut self, val: Style) -> Self { 123 | self.remove_style = val; 124 | self 125 | } 126 | 127 | /// Style of removed whitespace 128 | pub fn set_remove_whitespace_style(mut self, val: Style) -> Self { 129 | self.remove_whitespace_style = val; 130 | self 131 | } 132 | 133 | /// Set output separator 134 | pub fn set_separator(mut self, val: &'a str) -> Self { 135 | self.separator = val; 136 | self 137 | } 138 | 139 | /// Returns Vec of changes 140 | pub fn diff(&self) -> Vec> { 141 | basic::diff(&self.old, &self.new) 142 | } 143 | 144 | fn apply_style(&self, style: Style, whitespace_style: Style, a: &[&str]) -> String { 145 | let s = a.join(self.separator); 146 | if self.highlight_whitespace { 147 | collect_strings(split_by_char_fn(&s, |c| c.is_whitespace()).map(|s| { 148 | let style = if s 149 | .chars() 150 | .next() 151 | .map_or_else(|| false, |c| c.is_whitespace()) 152 | { 153 | whitespace_style 154 | } else { 155 | style 156 | }; 157 | s.style(style).to_string() 158 | })) 159 | .join("") 160 | } else { 161 | s.style(style).to_string() 162 | } 163 | } 164 | 165 | fn remove_color(&self, a: &[&str]) -> String { 166 | self.apply_style(self.remove_style, self.remove_whitespace_style, a) 167 | } 168 | 169 | fn insert_color(&self, a: &[&str]) -> String { 170 | self.apply_style(self.insert_style, self.insert_whitespace_style, a) 171 | } 172 | /// Returns formatted string with colors 173 | pub fn format(&self) -> String { 174 | let diff = self.diff(); 175 | let mut out: Vec = Vec::with_capacity(diff.len()); 176 | for op in diff { 177 | match op { 178 | basic::DiffOp::Equal(a) => out.push(a.join(self.separator)), 179 | basic::DiffOp::Insert(a) => out.push(self.insert_color(a)), 180 | basic::DiffOp::Remove(a) => out.push(self.remove_color(a)), 181 | basic::DiffOp::Replace(a, b) => { 182 | out.push(self.remove_color(a)); 183 | out.push(self.insert_color(b)); 184 | } 185 | } 186 | } 187 | out.join(self.separator) 188 | } 189 | } 190 | 191 | impl fmt::Display for InlineChangeset<'_> { 192 | fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 193 | write!(formatter, "{}", self.format()) 194 | } 195 | } 196 | 197 | pub fn diff_chars<'a>(old: &'a str, new: &'a str) -> InlineChangeset<'a> { 198 | let old: Vec<&str> = old.split("").filter(|&i| !i.is_empty()).collect(); 199 | let new: Vec<&str> = new.split("").filter(|&i| !i.is_empty()).collect(); 200 | 201 | InlineChangeset::new(old, new) 202 | } 203 | 204 | /// Diff two strings by words (contiguous) 205 | pub fn diff_words<'a>(old: &'a str, new: &'a str) -> InlineChangeset<'a> { 206 | InlineChangeset::new(split_words(old).collect(), split_words(new).collect()) 207 | } 208 | 209 | #[cfg(feature = "prettytable-rs")] 210 | fn color_multilines(color: AnsiColors, s: &str) -> String { 211 | collect_strings(s.split('\n').map(|i| i.color(color).to_string())).join("\n") 212 | } 213 | 214 | #[derive(Debug)] 215 | pub struct ContextConfig<'a> { 216 | pub context_size: usize, 217 | pub skipping_marker: &'a str, 218 | } 219 | 220 | /// Container for line-by-line text diff result. Can be pretty-printed by Display trait. 221 | #[derive(Debug, PartialEq, Eq)] 222 | pub struct LineChangeset<'a> { 223 | old: Vec<&'a str>, 224 | new: Vec<&'a str>, 225 | 226 | names: Option<(&'a str, &'a str)>, 227 | diff_only: bool, 228 | show_lines: bool, 229 | trim_new_lines: bool, 230 | aling_new_lines: bool, 231 | } 232 | 233 | impl<'a> LineChangeset<'a> { 234 | pub fn new(old: Vec<&'a str>, new: Vec<&'a str>) -> LineChangeset<'a> { 235 | LineChangeset { 236 | old, 237 | new, 238 | names: None, 239 | diff_only: false, 240 | show_lines: true, 241 | trim_new_lines: true, 242 | aling_new_lines: false, 243 | } 244 | } 245 | 246 | /// Sets names for side-by-side diff 247 | pub fn names(mut self, old: &'a str, new: &'a str) -> Self { 248 | self.names = Some((old, new)); 249 | self 250 | } 251 | /// Show only differences for side-by-side diff 252 | pub fn set_diff_only(mut self, val: bool) -> Self { 253 | self.diff_only = val; 254 | self 255 | } 256 | /// Show lines in side-by-side diff 257 | pub fn set_show_lines(mut self, val: bool) -> Self { 258 | self.show_lines = val; 259 | self 260 | } 261 | /// Trim new lines in side-by-side diff 262 | pub fn set_trim_new_lines(mut self, val: bool) -> Self { 263 | self.trim_new_lines = val; 264 | self 265 | } 266 | /// Align new lines inside diff 267 | pub fn set_align_new_lines(mut self, val: bool) -> Self { 268 | self.aling_new_lines = val; 269 | self 270 | } 271 | /// Returns Vec of changes 272 | pub fn diff(&self) -> Vec> { 273 | basic::diff(&self.old, &self.new) 274 | } 275 | 276 | #[cfg(feature = "prettytable-rs")] 277 | fn prettytable_process(&self, a: &[&str], color: Option) -> (String, usize) { 278 | let mut start = 0; 279 | let mut stop = a.len(); 280 | if self.trim_new_lines { 281 | for (index, element) in a.iter().enumerate() { 282 | if !element.is_empty() { 283 | break; 284 | } 285 | start = index + 1; 286 | } 287 | for (index, element) in a.iter().enumerate().rev() { 288 | if !element.is_empty() { 289 | stop = index + 1; 290 | break; 291 | } 292 | } 293 | } 294 | let out = &a[start..stop]; 295 | if let Some(color) = color { 296 | ( 297 | collect_strings(out.iter().map(|i| (*i).color(color))) 298 | .join("\n") 299 | .replace('\t', " "), 300 | start, 301 | ) 302 | } else { 303 | (out.join("\n").replace('\t', " "), start) 304 | } 305 | } 306 | 307 | #[cfg(feature = "prettytable-rs")] 308 | fn prettytable_process_replace( 309 | &self, 310 | old: &[&str], 311 | new: &[&str], 312 | ) -> ((String, String), (usize, usize)) { 313 | // White is dummy argument 314 | let (old, old_offset) = self.prettytable_process(old, None); 315 | let (new, new_offset) = self.prettytable_process(new, None); 316 | 317 | let mut old_out = String::new(); 318 | let mut new_out = String::new(); 319 | 320 | for op in diff_words(&old, &new).diff() { 321 | match op { 322 | basic::DiffOp::Equal(a) => { 323 | old_out.push_str(&a.join("")); 324 | new_out.push_str(&a.join("")); 325 | } 326 | basic::DiffOp::Insert(a) => { 327 | new_out.push_str(&color_multilines(Green, &a.join(""))); 328 | } 329 | basic::DiffOp::Remove(a) => { 330 | old_out.push_str(&color_multilines(Red, &a.join(""))); 331 | } 332 | basic::DiffOp::Replace(a, b) => { 333 | old_out.push_str(&color_multilines(Red, &a.join(""))); 334 | new_out.push_str(&color_multilines(Green, &b.join(""))); 335 | } 336 | } 337 | } 338 | 339 | ((old_out, new_out), (old_offset, new_offset)) 340 | } 341 | 342 | #[cfg(feature = "prettytable-rs")] 343 | fn prettytable_mktable(&self) -> prettytable::Table { 344 | let mut table = format_table::new(); 345 | if let Some((old, new)) = &self.names { 346 | let mut header = vec![]; 347 | if self.show_lines { 348 | header.push(Cell::new("")); 349 | } 350 | header.push(Cell::new(&old.cyan().to_string())); 351 | if self.show_lines { 352 | header.push(Cell::new("")); 353 | } 354 | header.push(Cell::new(&new.cyan().to_string())); 355 | table.set_titles(Row::new(header)); 356 | } 357 | let mut old_lines = 1; 358 | let mut new_lines = 1; 359 | let mut out: Vec<(usize, String, usize, String)> = Vec::new(); 360 | for op in &self.diff() { 361 | match op { 362 | basic::DiffOp::Equal(a) => { 363 | let (old, offset) = self.prettytable_process(a, None); 364 | if !self.diff_only { 365 | out.push((old_lines + offset, old.clone(), new_lines + offset, old)); 366 | } 367 | old_lines += a.len(); 368 | new_lines += a.len(); 369 | } 370 | basic::DiffOp::Insert(a) => { 371 | let (new, offset) = self.prettytable_process(a, Some(Green)); 372 | out.push((old_lines, "".to_string(), new_lines + offset, new)); 373 | new_lines += a.len(); 374 | } 375 | basic::DiffOp::Remove(a) => { 376 | let (old, offset) = self.prettytable_process(a, Some(Red)); 377 | out.push((old_lines + offset, old, new_lines, "".to_string())); 378 | old_lines += a.len(); 379 | } 380 | basic::DiffOp::Replace(a, b) => { 381 | let ((old, new), (old_offset, new_offset)) = 382 | self.prettytable_process_replace(a, b); 383 | out.push((old_lines + old_offset, old, new_lines + new_offset, new)); 384 | old_lines += a.len(); 385 | new_lines += b.len(); 386 | } 387 | }; 388 | } 389 | for (old_lines, old, new_lines, new) in out { 390 | if self.trim_new_lines && old.trim() == "" && new.trim() == "" { 391 | continue; 392 | } 393 | if self.show_lines { 394 | table.add_row(row![old_lines, old, new_lines, new]); 395 | } else { 396 | table.add_row(row![old, new]); 397 | } 398 | } 399 | table 400 | } 401 | 402 | #[cfg(feature = "prettytable-rs")] 403 | /// Prints side-by-side diff in table 404 | pub fn prettytable(&self) { 405 | let table = self.prettytable_mktable(); 406 | table.printstd(); 407 | } 408 | 409 | #[cfg(feature = "prettytable-rs")] 410 | /// Write side-by-side diff in table to any Writer. 411 | pub fn write_prettytable(&self, f: &mut W) -> std::io::Result 412 | where 413 | W: std::io::Write + std::io::IsTerminal, 414 | { 415 | let table = self.prettytable_mktable(); 416 | table.print(f) 417 | } 418 | 419 | fn remove_color(&self, a: &str) -> String { 420 | a.red().strikethrough().to_string() 421 | } 422 | 423 | fn insert_color(&self, a: &str) -> String { 424 | a.green().to_string() 425 | } 426 | 427 | /// Returns formatted string with colors 428 | pub fn format(&self) -> String { 429 | self.format_with_context(None, false) 430 | } 431 | 432 | /// Formats lines in DiffOp::Equal 433 | fn format_equal( 434 | &self, 435 | lines: &[&str], 436 | display_line_numbers: bool, 437 | prefix_size: usize, 438 | line_counter: &mut usize, 439 | ) -> Option { 440 | lines 441 | .iter() 442 | .map(|line| { 443 | let res = if display_line_numbers { 444 | format!("{} ", *line_counter) 445 | .pad_to_width_with_alignment(prefix_size, Alignment::Right) 446 | + line 447 | } else { 448 | "".pad_to_width(prefix_size) + line 449 | }; 450 | *line_counter += 1; 451 | res 452 | }) 453 | .reduce(|acc, line| acc + "\n" + &line) 454 | } 455 | 456 | /// Formats lines in DiffOp::Remove 457 | fn format_remove( 458 | &self, 459 | lines: &[&str], 460 | display_line_numbers: bool, 461 | prefix_size: usize, 462 | line_counter: &mut usize, 463 | ) -> String { 464 | lines 465 | .iter() 466 | .map(|line| { 467 | let res = if display_line_numbers { 468 | format!("{} ", *line_counter) 469 | .pad_to_width_with_alignment(prefix_size, Alignment::Right) 470 | + &self.remove_color(line) 471 | } else { 472 | "".pad_to_width(prefix_size) + &self.remove_color(line) 473 | }; 474 | *line_counter += 1; 475 | res 476 | }) 477 | .reduce(|acc, line| acc + "\n" + &line) 478 | .unwrap() 479 | } 480 | 481 | /// Formats lines in DiffOp::Insert 482 | fn format_insert(&self, lines: &[&str], prefix_size: usize) -> String { 483 | lines 484 | .iter() 485 | .map(|line| "".pad_to_width(prefix_size) + &self.insert_color(line)) 486 | .reduce(|acc, line| acc + "\n" + &line) 487 | .unwrap() 488 | } 489 | 490 | /// Returns formatted string with colors. 491 | /// May omit identical lines, if `context_size` is `Some(k)`. 492 | /// In this case, only print identical lines if they are within `k` lines 493 | /// of a changed line (as in `diff -C`). 494 | pub fn format_with_context( 495 | &self, 496 | context_config: Option, 497 | display_line_numbers: bool, 498 | ) -> String { 499 | let line_number_size = if display_line_numbers { 500 | (self.old.len() as f64).log10().ceil() as usize 501 | } else { 502 | 0 503 | }; 504 | let skipping_marker_size = if let Some(ContextConfig { 505 | skipping_marker, .. 506 | }) = context_config 507 | { 508 | skipping_marker.len() 509 | } else { 510 | 0 511 | }; 512 | let prefix_size = max(line_number_size, skipping_marker_size) + 1; 513 | 514 | let mut next_line = 1; 515 | 516 | let mut diff = self.diff().into_iter().peekable(); 517 | let mut out: Vec = Vec::with_capacity(diff.len()); 518 | let mut at_beginning = true; 519 | while let Some(op) = diff.next() { 520 | match op { 521 | basic::DiffOp::Equal(a) => match context_config { 522 | None => out.push(a.join("\n")), 523 | Some(ContextConfig { 524 | context_size, 525 | skipping_marker, 526 | }) => { 527 | let mut lines = a; 528 | if !at_beginning { 529 | let upper_bound = min(context_size, lines.len()); 530 | if let Some(newlines) = self.format_equal( 531 | &lines[..upper_bound], 532 | display_line_numbers, 533 | prefix_size, 534 | &mut next_line, 535 | ) { 536 | out.push(newlines) 537 | } 538 | lines = &lines[upper_bound..]; 539 | } 540 | if lines.is_empty() { 541 | continue; 542 | } 543 | let lower_bound = if lines.len() > context_size { 544 | lines.len() - context_size 545 | } else { 546 | 0 547 | }; 548 | if lower_bound > 0 { 549 | out.push(skipping_marker.to_string()); 550 | next_line += lower_bound 551 | } 552 | if diff.peek().is_none() { 553 | continue; 554 | } 555 | if let Some(newlines) = self.format_equal( 556 | &lines[lower_bound..], 557 | display_line_numbers, 558 | prefix_size, 559 | &mut next_line, 560 | ) { 561 | out.push(newlines) 562 | } 563 | } 564 | }, 565 | basic::DiffOp::Insert(a) => out.push(self.format_insert(a, prefix_size)), 566 | basic::DiffOp::Remove(a) => out.push(self.format_remove( 567 | a, 568 | display_line_numbers, 569 | prefix_size, 570 | &mut next_line, 571 | )), 572 | basic::DiffOp::Replace(a, b) => { 573 | out.push(self.format_remove( 574 | a, 575 | display_line_numbers, 576 | prefix_size, 577 | &mut next_line, 578 | )); 579 | out.push(self.format_insert(b, prefix_size)); 580 | } 581 | } 582 | at_beginning = false; 583 | } 584 | out.join("\n") 585 | } 586 | } 587 | 588 | impl fmt::Display for LineChangeset<'_> { 589 | fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 590 | write!(formatter, "{}", self.format()) 591 | } 592 | } 593 | 594 | pub fn diff_lines<'a>(old: &'a str, new: &'a str) -> LineChangeset<'a> { 595 | let old: Vec<&str> = old.lines().collect(); 596 | let new: Vec<&str> = new.lines().collect(); 597 | 598 | LineChangeset::new(old, new) 599 | } 600 | 601 | fn _test_splitter_basic(text: &str, exp: &[&str]) { 602 | let res = 603 | collect_strings(split_by_char_fn(text, |c: char| c.is_whitespace()).map(|s| s.to_string())); 604 | assert_eq!(res, exp) 605 | } 606 | 607 | #[test] 608 | fn test_splitter() { 609 | _test_splitter_basic( 610 | " blah test2 test3 ", 611 | &[" ", " ", "blah", " ", "test2", " ", "test3", " ", " "], 612 | ); 613 | _test_splitter_basic( 614 | "\tblah test2 test3 ", 615 | &["\t", "blah", " ", "test2", " ", "test3", " ", " "], 616 | ); 617 | _test_splitter_basic( 618 | "\tblah test2 test3 t", 619 | &["\t", "blah", " ", "test2", " ", "test3", " ", " ", "t"], 620 | ); 621 | _test_splitter_basic( 622 | "\tblah test2 test3 tt", 623 | &["\t", "blah", " ", "test2", " ", "test3", " ", " ", "tt"], 624 | ); 625 | } 626 | 627 | #[test] 628 | fn test_basic() { 629 | println!("diff_chars: {}", diff_chars("abefcd", "zadqwc")); 630 | println!( 631 | "diff_chars: {}", 632 | diff_chars( 633 | "The quick brown fox jumps over the lazy dog", 634 | "The quick brown dog leaps over the lazy cat" 635 | ) 636 | ); 637 | println!( 638 | "diff_chars: {}", 639 | diff_chars( 640 | "The red brown fox jumped over the rolling log", 641 | "The brown spotted fox leaped over the rolling log" 642 | ) 643 | ); 644 | println!( 645 | "diff_chars: {}", 646 | diff_chars( 647 | "The red brown fox jumped over the rolling log", 648 | "The brown spotted fox leaped over the rolling log" 649 | ) 650 | .set_highlight_whitespace(true) 651 | ); 652 | println!( 653 | "diff_words: {}", 654 | diff_words( 655 | "The red brown fox jumped over the rolling log", 656 | "The brown spotted fox leaped over the rolling log" 657 | ) 658 | ); 659 | println!( 660 | "diff_words: {}", 661 | diff_words( 662 | "The quick brown fox jumps over the lazy dog", 663 | "The quick, brown dog leaps over the lazy cat" 664 | ) 665 | ); 666 | } 667 | 668 | #[test] 669 | fn test_split_words() { 670 | assert_eq!( 671 | collect_strings(split_words("Hello World")), 672 | ["Hello", " ", "World"] 673 | ); 674 | assert_eq!( 675 | collect_strings(split_words("Hello😋World")), 676 | ["Hello", "😋", "World"] 677 | ); 678 | assert_eq!( 679 | collect_strings(split_words( 680 | "The red brown fox\tjumped, over the rolling log" 681 | )), 682 | [ 683 | "The", " ", "red", " ", "brown", " ", "fox", "\t", "jumped", ",", " ", "over", " ", 684 | "the", " ", "rolling", " ", "log" 685 | ] 686 | ); 687 | } 688 | 689 | #[test] 690 | fn test_diff_lines() { 691 | let code1_a = r#" 692 | void func1() { 693 | x += 1 694 | } 695 | 696 | void func2() { 697 | x += 2 698 | } 699 | "#; 700 | let code1_b = r#" 701 | void func1(a: u32) { 702 | x += 1 703 | } 704 | 705 | void functhreehalves() { 706 | x += 1.5 707 | } 708 | 709 | void func2() { 710 | x += 2 711 | } 712 | 713 | void func3(){} 714 | "#; 715 | println!("diff_lines:"); 716 | println!("{}", diff_lines(code1_a, code1_b)); 717 | println!("===="); 718 | diff_lines(code1_a, code1_b) 719 | .names("left", "right") 720 | .set_align_new_lines(true) 721 | .prettytable(); 722 | } 723 | 724 | fn _test_colors(changeset: &InlineChangeset, exp: &[(Option