Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
205 changes: 204 additions & 1 deletion diff.carp
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,31 @@
[(Eq (Array.slice new ssn (+ ssn sl)))]])
(diff &(Array.suffix old (+ sso sl)) &(Array.suffix new (+ ssn sl)))]))))

(doc str "Returns a string representation of a Diff entry.")
(defn str [d]
(match-ref d
(Eq xs) (String.concat &[@"(Eq " (str xs) (Char.str \))])
(Insertion xs) (String.concat &[@"(Insertion " (str xs) (Char.str \))])
(Deletion xs) (String.concat &[@"(Deletion " (str xs) (Char.str \))])))
(implements str Diff.str)

(doc prn
"Returns a string representation of a Diff entry suitable for printing.")
(defn prn [d]
(match-ref d
(Eq xs) (String.concat &[@"(Eq " (prn xs) (Char.str \))])
(Insertion xs) (String.concat &[@"(Insertion " (prn xs) (Char.str \))])
(Deletion xs) (String.concat &[@"(Deletion " (prn xs) (Char.str \))])))
(implements prn Diff.prn)

(defn string-diff [old new] (diff &(String.words old) &(String.words new)))

(doc line-diff "Diffs two strings line by line.")
(defn line-diff [old new] (diff &(String.lines old) &(String.lines new)))

(doc char-diff "Diffs two strings character by character.")
(defn char-diff [old new] (diff &(String.chars old) &(String.chars new)))

(defn eq? [d] (match @d (Eq _) true _ false))

(defn inserted? [d] (match @d (Insertion _) true _ false))
Expand All @@ -58,4 +81,184 @@

(defn eq [d] (Array.copy-filter &eq? d))
(defn insertions [d] (Array.copy-filter &inserted? d))
(defn deletions [d] (Array.copy-filter &deleted? d)))
(defn deletions [d] (Array.copy-filter &deleted? d))

(private reorder-changes)
(hidden reorder-changes)
(defn reorder-changes [raw]
(let-do [result (the (Array (Diff (Array String))) [])
n (Array.length raw)
i 0]
(while (< i n)
(if (match-ref (Array.unsafe-nth raw i) (Eq _) true _ false)
(do
(match-ref (Array.unsafe-nth raw i)
(Eq ls) (Array.push-back! &result (Eq @ls))
_ ())
(set! i (Int.inc i)))
(let-do [dels (the (Array String) [])
inss (the (Array String) [])]
(while-do (and (< i n)
(match-ref (Array.unsafe-nth raw i)
(Eq _) false
_ true))
(match-ref (Array.unsafe-nth raw i)
(Deletion ls)
(for [j 0 (Array.length ls)]
(Array.push-back! &dels @(Array.unsafe-nth ls j)))
(Insertion ls)
(for [j 0 (Array.length ls)]
(Array.push-back! &inss @(Array.unsafe-nth ls j)))
_ ())
(set! i (Int.inc i)))
(when (> (Array.length &dels) 0)
(Array.push-back! &result (Deletion dels)))
(when (> (Array.length &inss) 0)
(Array.push-back! &result (Insertion inss))))))
result))

(private render-hunk-header)
(hidden render-hunk-header)
(defn render-hunk-header [old-start old-count new-start new-count]
(String.concat
&[@"@@ -"
(str old-start)
@","
(str old-count)
@" +"
(str new-start)
@","
(str new-count)
@" @@
"]))

(doc unified-diff
"Renders a unified diff between two strings with hunk headers.
Uses 3 lines of context around changes.")
(defn unified-diff [old new]
(let-do [raw (line-diff old new)
d (reorder-changes &raw)
n (Array.length &d)
context 3
result (the (Array String) [])
hunk-parts (the (Array String) [])
hunk-old-start 0
hunk-old-count 0
hunk-new-start 0
hunk-new-count 0
has-changes false
old-pos 1
new-pos 1
pending-ctx (the (Array String) [])]
(for [i 0 n]
(match-ref (Array.unsafe-nth &d i)
(Eq ls)
(let [len (Array.length ls)]
(if (not has-changes)
(do
(set! pending-ctx (Array.suffix ls (max 0 (- len context))))
(set! old-pos (+ old-pos len))
(set! new-pos (+ new-pos len)))
(if (> len (* 2 context))
(let-do [trailing (min context len)]
(for [j 0 trailing]
(do
(Array.push-back! &hunk-parts
(String.concat &[@" "
@(Array.unsafe-nth ls
j)
@"
"]))
(set! hunk-old-count (Int.inc hunk-old-count))
(set! hunk-new-count (Int.inc hunk-new-count))))
(Array.push-back! &result
(render-hunk-header hunk-old-start
hunk-old-count
hunk-new-start
hunk-new-count))
(for [j 0 (Array.length &hunk-parts)]
(Array.push-back! &result
@(Array.unsafe-nth &hunk-parts j)))
(set! hunk-parts [])
(set! hunk-old-count 0)
(set! hunk-new-count 0)
(set! has-changes false)
(set! pending-ctx (Array.suffix ls (max 0 (- len context))))
(set! old-pos (+ old-pos len))
(set! new-pos (+ new-pos len)))
(do
(for [j 0 len]
(do
(Array.push-back! &hunk-parts
(String.concat &[@" "
@(Array.unsafe-nth ls
j)
@"
"]))
(set! hunk-old-count (Int.inc hunk-old-count))
(set! hunk-new-count (Int.inc hunk-new-count))))
(set! old-pos (+ old-pos len))
(set! new-pos (+ new-pos len))))))
(Deletion ls)
(let-do [len (Array.length ls)]
(unless has-changes
(let-do [ctx-len (Array.length &pending-ctx)]
(set! hunk-old-start (- old-pos ctx-len))
(set! hunk-new-start (- new-pos ctx-len))
(for [j 0 ctx-len]
(do
(Array.push-back! &hunk-parts
(String.concat
&[@" "
@(Array.unsafe-nth &pending-ctx j)
@"
"]))
(set! hunk-old-count (Int.inc hunk-old-count))
(set! hunk-new-count (Int.inc hunk-new-count))))
(set! pending-ctx [])
(set! has-changes true)))
(for [j 0 len]
(do
(Array.push-back! &hunk-parts
(String.concat &[@"-"
@(Array.unsafe-nth ls j)
@"
"]))
(set! hunk-old-count (Int.inc hunk-old-count))))
(set! old-pos (+ old-pos len)))
(Insertion ls)
(let-do [len (Array.length ls)]
(unless has-changes
(let-do [ctx-len (Array.length &pending-ctx)]
(set! hunk-old-start (- old-pos ctx-len))
(set! hunk-new-start (- new-pos ctx-len))
(for [j 0 ctx-len]
(do
(Array.push-back! &hunk-parts
(String.concat
&[@" "
@(Array.unsafe-nth &pending-ctx j)
@"
"]))
(set! hunk-old-count (Int.inc hunk-old-count))
(set! hunk-new-count (Int.inc hunk-new-count))))
(set! pending-ctx [])
(set! has-changes true)))
(for [j 0 len]
(do
(Array.push-back! &hunk-parts
(String.concat &[@"+"
@(Array.unsafe-nth ls j)
@"
"]))
(set! hunk-new-count (Int.inc hunk-new-count))))
(set! new-pos (+ new-pos len)))))
(when-do has-changes
(Array.push-back! &result
(render-hunk-header hunk-old-start
hunk-old-count
hunk-new-start
hunk-new-count))
(for [j 0 (Array.length &hunk-parts)]
(Array.push-back! &result @(Array.unsafe-nth &hunk-parts j))))
(String.concat &result))))
166 changes: 165 additions & 1 deletion tests/diff.carp
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,168 @@
(assert-equal test
&[(Deletion [1 2]) (Eq [3 4])]
&(diff &[1 2 3 4] &[3 4])
"new is a suffix of old")))
"new is a suffix of old")

; str
(assert-equal test "(Eq [1 2 3])" &(str &(Eq [1 2 3])) "str of Eq")
(assert-equal test
"(Insertion [4 5])"
&(str &(Insertion [4 5]))
"str of Insertion")
(assert-equal test "(Deletion [1])" &(str &(Deletion [1])) "str of Deletion")

; prn
(assert-equal test
"(Eq [@\"hello\" @\"world\"])"
&(prn &(Eq [@"hello" @"world"]))
"prn of Eq with strings shows quotes")

; line-diff
(assert-equal test
&[(Eq [@"a"]) (Insertion [@"X"]) (Deletion [@"b"]) (Eq [@"c"])]
&(line-diff "a
b
c" "a
X
c")
"line-diff splits by newlines")
(assert-equal test &[(Eq [@"a" @"b" @"c"])] &(line-diff "a
b
c" "a
b
c") "line-diff of identical strings")

; char-diff
(assert-equal test
&[(Eq [\h]) (Insertion [\a]) (Deletion [\e]) (Eq [\l \l \o])]
&(char-diff "hello" "hallo")
"char-diff splits by character")

; unified-diff: simple change
(assert-equal test "@@ -1,3 +1,3 @@
a
-b
+X
c
" &(unified-diff "a
b
c" "a
X
c") "unified-diff simple replacement")

; unified-diff: identical strings produce empty output
(assert-equal test "" &(unified-diff "a
b
c" "a
b
c") "unified-diff of identical strings is empty")

; unified-diff: insertion only
(assert-equal test "@@ -1,2 +1,3 @@
a
+X
b
" &(unified-diff "a
b" "a
X
b") "unified-diff insertion")

; unified-diff: deletion only
(assert-equal test "@@ -1,3 +1,2 @@
a
-X
b
" &(unified-diff "a
X
b" "a
b") "unified-diff deletion")

; unified-diff: change at start
(assert-equal test "@@ -1,4 +1,4 @@
-X
+Y
b
c
d
" &(unified-diff "X
b
c
d" "Y
b
c
d") "unified-diff change at start")

; unified-diff: change at end
(assert-equal test "@@ -1,4 +1,4 @@
a
b
c
-X
+Y
" &(unified-diff "a
b
c
X" "a
b
c
Y") "unified-diff change at end")

; unified-diff: completely different
(assert-equal test "@@ -1,3 +1,3 @@
-a
-b
-c
+X
+Y
+Z
" &(unified-diff "a
b
c" "X
Y
Z") "unified-diff completely different")

; unified-diff: two hunks (changes > 2*context apart)
(assert-equal test "@@ -1,6 +1,6 @@
1
2
-3
+X
4
5
6
@@ -11,5 +11,5 @@
11
12
13
-14
+Y
15
" &(unified-diff "1
2
3
4
5
6
7
8
9
10
11
12
13
14
15" "1
2
X
4
5
6
7
8
9
10
11
12
13
Y
15") "unified-diff two hunks")))
Loading