Make missing newline hint in unified diff configurable
This commit is contained in:
parent
4db3f2f77e
commit
a9a11e9099
5 changed files with 107 additions and 11 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
use std::iter;
|
use std::{fmt, iter};
|
||||||
|
|
||||||
use crate::algorithms::{Algorithm, DiffOp, DiffTag};
|
use crate::algorithms::{Algorithm, DiffOp, DiffTag};
|
||||||
use crate::text::{Change, ChangeTag, TextDiff};
|
use crate::text::{Change, ChangeTag, TextDiff};
|
||||||
|
|
@ -30,6 +30,9 @@ impl<'a, 's> MultiIndex<'a, 's> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents the expanded textual change with inline highlights.
|
||||||
|
///
|
||||||
|
/// This is like [`Change`] but with inline highlight info.
|
||||||
#[derive(Debug, PartialEq, Eq, Hash, Clone, Ord, PartialOrd)]
|
#[derive(Debug, PartialEq, Eq, Hash, Clone, Ord, PartialOrd)]
|
||||||
pub struct InlineChange<'s> {
|
pub struct InlineChange<'s> {
|
||||||
tag: ChangeTag,
|
tag: ChangeTag,
|
||||||
|
|
@ -74,11 +77,28 @@ impl<'s> From<Change<'s>> for InlineChange<'s> {
|
||||||
old_index: change.old_index(),
|
old_index: change.old_index(),
|
||||||
new_index: change.old_index(),
|
new_index: change.old_index(),
|
||||||
values: vec![(false, change.value())],
|
values: vec![(false, change.value())],
|
||||||
missing_newline: change.is_missing_newline(),
|
missing_newline: change.missing_newline(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'s> fmt::Display for InlineChange<'s> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
for &(emphasized, value) in &self.values {
|
||||||
|
let marker = match (emphasized, self.tag) {
|
||||||
|
(false, _) | (true, ChangeTag::Equal) => "",
|
||||||
|
(true, ChangeTag::Delete) => "-",
|
||||||
|
(true, ChangeTag::Insert) => "+",
|
||||||
|
};
|
||||||
|
write!(f, "{}{}{}", marker, value, marker)?;
|
||||||
|
}
|
||||||
|
if self.missing_newline {
|
||||||
|
writeln!(f)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn iter_inline_changes<'diff>(
|
pub(crate) fn iter_inline_changes<'diff>(
|
||||||
diff: &'diff TextDiff,
|
diff: &'diff TextDiff,
|
||||||
op: &DiffOp,
|
op: &DiffOp,
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,24 @@
|
||||||
//!
|
//!
|
||||||
//! Because the [`TextDiff::grouped_ops`] method can isolate clusters of changes
|
//! Because the [`TextDiff::grouped_ops`] method can isolate clusters of changes
|
||||||
//! this even works for very long files if paired with this method.
|
//! this even works for very long files if paired with this method.
|
||||||
|
//!
|
||||||
|
//! ## Trailing Newlines
|
||||||
|
//!
|
||||||
|
//! When working with line diffs (and unified diffs in general) there are two
|
||||||
|
//! "philosophies" to look at lines. One is to diff lines without their newline
|
||||||
|
//! character, the other is to diff with the newline character. Typically the
|
||||||
|
//! latter is done because text files do not _have_ to end in a newline character.
|
||||||
|
//! As a result there is a difference between `foo\n` and `foo` as far as diffs
|
||||||
|
//! are concerned.
|
||||||
|
//!
|
||||||
|
//! In similar this is handled on the [`Change`] or [`InlineChange`] level. If
|
||||||
|
//! a diff was created via [`TextDiff::from_lines`] the text diffing system is
|
||||||
|
//! instructed to check if there are missing newlines encountered. If that is
|
||||||
|
//! the case the [`Change`] object will return true from the
|
||||||
|
//! [`Change::missing_newline`] method so the caller knows to handle this by
|
||||||
|
//! either rendering a virtual newline at that position or to indicate it in
|
||||||
|
//! different ways. For instance the unified diff code will render the special
|
||||||
|
//! `\ No newline at end of file` marker.
|
||||||
#![cfg(feature = "text")]
|
#![cfg(feature = "text")]
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::cmp::Reverse;
|
use std::cmp::Reverse;
|
||||||
|
|
@ -253,7 +271,7 @@ impl<'s> Change<'s> {
|
||||||
///
|
///
|
||||||
/// The [`std::fmt::Display`] implementation of [`Change`] will automatically
|
/// The [`std::fmt::Display`] implementation of [`Change`] will automatically
|
||||||
/// insert a newline after the value if this is true.
|
/// insert a newline after the value if this is true.
|
||||||
pub fn is_missing_newline(&self) -> bool {
|
pub fn missing_newline(&self) -> bool {
|
||||||
self.missing_newline
|
self.missing_newline
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -364,11 +382,6 @@ impl<'old, 'new, 'bufs> TextDiff<'old, 'new, 'bufs> {
|
||||||
/// ways in which a change could be encoded (insert/delete vs replace), look
|
/// ways in which a change could be encoded (insert/delete vs replace), look
|
||||||
/// up the value from the appropriate slice and also handle correct index
|
/// up the value from the appropriate slice and also handle correct index
|
||||||
/// handling.
|
/// handling.
|
||||||
///
|
|
||||||
/// In addition it has some custom handling to insert "virtual" newlines
|
|
||||||
/// for diffs where [`TextDiff::newline_terminated`] is `true` but the
|
|
||||||
/// diff does not end in newlines in the right places. For more information
|
|
||||||
/// see [`Change::is_virtual`].
|
|
||||||
pub fn iter_changes(&self, op: &DiffOp) -> impl Iterator<Item = Change> {
|
pub fn iter_changes(&self, op: &DiffOp) -> impl Iterator<Item = Change> {
|
||||||
let newline_terminated = self.newline_terminated;
|
let newline_terminated = self.newline_terminated;
|
||||||
let (tag, old_range, new_range) = op.as_tag_tuple();
|
let (tag, old_range, new_range) = op.as_tag_tuple();
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
---
|
||||||
|
source: src/text/udiff.rs
|
||||||
|
expression: "&diff.unified_diff().missing_newline_hint(false).header(\"a.txt\",\n \"b.txt\").to_string()"
|
||||||
|
---
|
||||||
|
--- a.txt
|
||||||
|
+++ b.txt
|
||||||
|
@@ -0 +0 @@
|
||||||
|
-a
|
||||||
|
+b
|
||||||
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
---
|
||||||
|
source: src/text/udiff.rs
|
||||||
|
expression: "&diff.unified_diff().header(\"a.txt\", \"b.txt\").to_string()"
|
||||||
|
---
|
||||||
|
--- a.txt
|
||||||
|
+++ b.txt
|
||||||
|
@@ -0 +0 @@
|
||||||
|
-a
|
||||||
|
+b
|
||||||
|
\ No newline at end of file
|
||||||
|
|
||||||
|
|
@ -81,6 +81,7 @@ impl fmt::Display for UnifiedHunkHeader {
|
||||||
pub struct UnifiedDiff<'diff, 'old, 'new, 'bufs> {
|
pub struct UnifiedDiff<'diff, 'old, 'new, 'bufs> {
|
||||||
diff: &'diff TextDiff<'old, 'new, 'bufs>,
|
diff: &'diff TextDiff<'old, 'new, 'bufs>,
|
||||||
context_radius: usize,
|
context_radius: usize,
|
||||||
|
missing_newline_hint: bool,
|
||||||
header: Option<(String, String)>,
|
header: Option<(String, String)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -90,6 +91,7 @@ impl<'diff, 'old, 'new, 'bufs> UnifiedDiff<'diff, 'old, 'new, 'bufs> {
|
||||||
UnifiedDiff {
|
UnifiedDiff {
|
||||||
diff,
|
diff,
|
||||||
context_radius: 3,
|
context_radius: 3,
|
||||||
|
missing_newline_hint: true,
|
||||||
header: None,
|
header: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -114,14 +116,25 @@ impl<'diff, 'old, 'new, 'bufs> UnifiedDiff<'diff, 'old, 'new, 'bufs> {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Controls the missing newline hint.
|
||||||
|
///
|
||||||
|
/// By default a special `\ No newline at end of file` marker is added to
|
||||||
|
/// the output when a file is not terminated with a final newline. This can
|
||||||
|
/// be disabled with this flag.
|
||||||
|
pub fn missing_newline_hint(&mut self, yes: bool) -> &mut Self {
|
||||||
|
self.missing_newline_hint = yes;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Iterates over all hunks as configured.
|
/// Iterates over all hunks as configured.
|
||||||
pub fn iter_hunks(&self) -> impl Iterator<Item = UnifiedDiffHunk<'diff, 'old, 'new, 'bufs>> {
|
pub fn iter_hunks(&self) -> impl Iterator<Item = UnifiedDiffHunk<'diff, 'old, 'new, 'bufs>> {
|
||||||
let diff = self.diff;
|
let diff = self.diff;
|
||||||
|
let missing_newline_hint = self.missing_newline_hint;
|
||||||
self.diff
|
self.diff
|
||||||
.grouped_ops(self.context_radius)
|
.grouped_ops(self.context_radius)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|ops| !ops.is_empty())
|
.filter(|ops| !ops.is_empty())
|
||||||
.map(move |ops| UnifiedDiffHunk::new(ops, diff))
|
.map(move |ops| UnifiedDiffHunk::new(ops, diff, missing_newline_hint))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn header_opt(&mut self, header: Option<(&str, &str)>) -> &mut Self {
|
fn header_opt(&mut self, header: Option<(&str, &str)>) -> &mut Self {
|
||||||
|
|
@ -138,6 +151,7 @@ impl<'diff, 'old, 'new, 'bufs> UnifiedDiff<'diff, 'old, 'new, 'bufs> {
|
||||||
pub struct UnifiedDiffHunk<'diff, 'old, 'new, 'bufs> {
|
pub struct UnifiedDiffHunk<'diff, 'old, 'new, 'bufs> {
|
||||||
diff: &'diff TextDiff<'old, 'new, 'bufs>,
|
diff: &'diff TextDiff<'old, 'new, 'bufs>,
|
||||||
ops: Vec<DiffOp>,
|
ops: Vec<DiffOp>,
|
||||||
|
missing_newline_hint: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'diff, 'old, 'new, 'bufs> UnifiedDiffHunk<'diff, 'old, 'new, 'bufs> {
|
impl<'diff, 'old, 'new, 'bufs> UnifiedDiffHunk<'diff, 'old, 'new, 'bufs> {
|
||||||
|
|
@ -145,8 +159,13 @@ impl<'diff, 'old, 'new, 'bufs> UnifiedDiffHunk<'diff, 'old, 'new, 'bufs> {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
ops: Vec<DiffOp>,
|
ops: Vec<DiffOp>,
|
||||||
diff: &'diff TextDiff<'old, 'new, 'bufs>,
|
diff: &'diff TextDiff<'old, 'new, 'bufs>,
|
||||||
|
missing_newline_hint: bool,
|
||||||
) -> UnifiedDiffHunk<'diff, 'old, 'new, 'bufs> {
|
) -> UnifiedDiffHunk<'diff, 'old, 'new, 'bufs> {
|
||||||
UnifiedDiffHunk { diff, ops }
|
UnifiedDiffHunk {
|
||||||
|
diff,
|
||||||
|
ops,
|
||||||
|
missing_newline_hint,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the header for the hunk.
|
/// Returns the header for the hunk.
|
||||||
|
|
@ -159,6 +178,11 @@ impl<'diff, 'old, 'new, 'bufs> UnifiedDiffHunk<'diff, 'old, 'new, 'bufs> {
|
||||||
&self.ops
|
&self.ops
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the value of the `missing_newline_hint` flag.
|
||||||
|
pub fn missing_newline_hint(&self) -> bool {
|
||||||
|
self.missing_newline_hint
|
||||||
|
}
|
||||||
|
|
||||||
/// Iterates over all changes in a hunk.
|
/// Iterates over all changes in a hunk.
|
||||||
pub fn iter_changes(&self) -> impl Iterator<Item = Change<'_>> + '_ {
|
pub fn iter_changes(&self) -> impl Iterator<Item = Change<'_>> + '_ {
|
||||||
// unclear why this needs Box::new here. It seems to infer some really
|
// unclear why this needs Box::new here. It seems to infer some really
|
||||||
|
|
@ -192,9 +216,16 @@ impl<'diff, 'old, 'new, 'bufs> fmt::Display for UnifiedDiffHunk<'diff, 'old, 'ne
|
||||||
ChangeTag::Delete => '-',
|
ChangeTag::Delete => '-',
|
||||||
ChangeTag::Insert => '+',
|
ChangeTag::Insert => '+',
|
||||||
},
|
},
|
||||||
change,
|
change.value(),
|
||||||
nl
|
nl
|
||||||
)?;
|
)?;
|
||||||
|
if change.missing_newline() {
|
||||||
|
if self.missing_newline_hint {
|
||||||
|
writeln!(f, "\n\\ No newline at end of file")?;
|
||||||
|
} else {
|
||||||
|
writeln!(f, "")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -247,3 +278,14 @@ fn test_empty_unified_diff() {
|
||||||
let diff = TextDiff::from_lines("abc", "abc");
|
let diff = TextDiff::from_lines("abc", "abc");
|
||||||
assert_eq!(diff.unified_diff().header("a.txt", "b.txt").to_string(), "");
|
assert_eq!(diff.unified_diff().header("a.txt", "b.txt").to_string(), "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_unified_diff_newline_hint() {
|
||||||
|
let diff = TextDiff::from_lines("a\n", "b");
|
||||||
|
insta::assert_snapshot!(&diff.unified_diff().header("a.txt", "b.txt").to_string());
|
||||||
|
insta::assert_snapshot!(&diff
|
||||||
|
.unified_diff()
|
||||||
|
.missing_newline_hint(false)
|
||||||
|
.header("a.txt", "b.txt")
|
||||||
|
.to_string());
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue