@@ -25,44 +25,79 @@ defmodule Diff do
2525 defstruct [ :element , :index , :length ]
2626 end
2727
28+ @ doc """
29+ Applies with patches with supplied annotation (top and tail)
30+ This is used to generate visual diffs, etc
31+ Shares the same code as patch
32+ """
33+ def annotated_patch ( original , patches , annotations , from_list_fn \\ fn ( list ) -> list end ) do
34+ apply_patches ( original , patches , annotations , from_list_fn )
35+ end
36+
2837 @ doc """
2938 Applies the patches from a previous diff to the given string.
3039 Will return the patched version as a list unless a from_list_fn/1 is supplied.
3140 This function will takes the patched list as input and outputs the result.
3241 """
3342 def patch ( original , patches , from_list_fn \\ fn ( list ) -> list end ) do
43+ apply_patches ( original , patches , [ ] , from_list_fn )
44+ end
45+
46+ defp apply_patches ( original , patches , annotations , from_list_fn ) do
3447 original = Diffable . to_list ( original )
3548
36- Enum . reduce ( patches , original , fn ( patch , changed ) ->
37- do_patch ( changed , patch )
38- end )
39- |> from_list_fn . ( )
49+ patchfn = fn ( patch , { increment , changed } ) ->
50+ do_patch ( { increment , changed } , patch , annotations )
51+ end
52+
53+ increment = 0
54+ { _ , returnlist } = Enum . reduce ( patches , { increment , original } , patchfn )
55+ from_list_fn . ( returnlist )
4056 end
4157
42- defp do_patch ( original , % Diff.Insert { element: element , index: index } ) do
43- { left , right } = Enum . split ( original , index )
44- left ++ element ++ right
58+ defp do_patch ( { incr , original } , % Diff.Insert { element: element , index: index } ,
59+ annotations ) do
60+ { left , right } = Enum . split ( original , index + incr )
61+ { newelement , newincr } = annotate ( element , :insert , annotations , incr )
62+ return = left ++ newelement ++ right
63+ { newincr , return }
4564 end
4665
47- defp do_patch ( original , % Diff.Delete { element: _ , index: index , length: length } ) do
48- { left , deleted } = Enum . split ( original , index )
49- { _ , right } = Enum . split ( deleted , length )
50- left ++ right
66+ defp do_patch ( { incr , original } , % Diff.Delete { element: element , index: index ,
67+ length: length } , annotations ) do
68+ { left , deleted } = Enum . split ( original , index + incr )
69+ { actuallydeleted , right } = Enum . split ( deleted , length )
70+ case element do
71+ ^ actuallydeleted ->
72+ { newelement , newincr } = annotate ( element , :deleted , annotations , incr )
73+ return = left ++ newelement ++ right
74+ { newincr , return }
75+ _other ->
76+ exit ( "failed delete" )
77+ end
5178 end
5279
53- defp do_patch ( original , % Diff.Modified { element: element , old_element: _ , index: index , length: length } ) do
54- { left , deleted } = Enum . split ( original , index )
80+ defp do_patch ( { incr , original } ,
81+ % Diff.Modified { element: element , old_element: _ ,
82+ index: index , length: length } ,
83+ annotations ) do
84+ { left , deleted } = Enum . split ( original , index + incr )
5585 { _ , right } = Enum . split ( deleted , length )
56- left ++ element ++ right
86+ { newelement , newincr } = annotate ( element , :modified , annotations , incr )
87+ return = left ++ newelement ++ right
88+ { newincr , return }
5789 end
5890
59- defp do_patch ( original , % Diff.Unchanged { } ) do
60- original
91+ defp do_patch ( { incr , original } , % Diff.Unchanged { } , _annotations ) do
92+ { incr , original }
6193 end
6294
63- defp do_patch ( original , % Diff.Ignored { element: element , index: index } ) do
64- { left , right } = Enum . split ( original , index )
65- left ++ element ++ right
95+ defp do_patch ( { incr , original } , % Diff.Ignored { element: element , index: index } ,
96+ annotations ) do
97+ { left , right } = Enum . split ( original , index + incr )
98+ { newelement , newincr } = annotate ( element , :ignored , annotations , incr )
99+ return = left ++ newelement ++ right
100+ { newincr , return }
66101 end
67102
68103 @ doc """
@@ -85,12 +120,15 @@ defmodule Diff do
85120 end
86121
87122 defp longest_common_subsequence ( x , y , x_length , y_length ) do
88- matrix = Matrix . new ( x_length + 1 , y_length + 1 )
89123
90- matrix = Enum . reduce ( 1 .. x_length , matrix , fn ( i , matrix ) ->
124+ matrix = Matrix . new ( x_length + 1 , y_length + 1 )
91125
92- Enum . reduce ( 1 .. y_length , matrix , fn ( j , matrix ) ->
126+ # a reduction over a 2D array requires a closure inside an anonymous function
127+ # sorry but there is nothing to be done about that
128+ rowreductionFn = fn ( i , matrix ) ->
93129
130+ # setup the second closure
131+ columnreductionFn = fn ( j , matrix ) ->
94132 if Enum . fetch! ( x , i - 1 ) == Enum . fetch! ( y , j - 1 ) do
95133 value = Matrix . get ( matrix , i - 1 , j - 1 )
96134 Matrix . put ( matrix , i , j , value + 1 )
@@ -100,89 +138,114 @@ defmodule Diff do
100138
101139 Matrix . put ( matrix , i , j , max ( original_value , changed_value ) )
102140 end
141+ end
103142
104- end )
143+ Enum . reduce ( 1 .. y_length , matrix , columnreductionFn )
144+
145+ end
105146
106- end )
147+ _matrix = Enum . reduce ( 1 .. x_length , matrix , rowreductionFn )
107148
108- matrix
109149 end
110150
111151 defp build_diff ( matrix , x , y , i , j , edits , options ) do
112152 cond do
113153 i > 0 and j > 0 and Enum . fetch! ( x , i - 1 ) == Enum . fetch! ( y , j - 1 ) ->
114- if Dict . get ( options , :keep_unchanged , false ) do
115- edits = edits ++ [ { :unchanged , Enum . fetch! ( x , i - 1 ) , i - 1 } ]
116- end
117-
118- build_diff ( matrix , x , y , i - 1 , j - 1 , edits , options )
154+ newedits = if Dict . get ( options , :keep_unchanged , false ) do
155+ edits ++ [ { :unchanged , Enum . fetch! ( x , i - 1 ) , i - 1 } ]
156+ else
157+ edits
158+ end
159+ build_diff ( matrix , x , y , i - 1 , j - 1 , newedits , options )
119160 j > 0 and ( i == 0 or Matrix . get ( matrix , i , j - 1 ) >= Matrix . get ( matrix , i - 1 , j ) ) ->
120- build_diff ( matrix , x , y , i , j - 1 , edits ++ [ { :insert , Enum . fetch! ( y , j - 1 ) , j - 1 } ] , options )
161+ newedit = { :insert , Enum . fetch! ( y , j - 1 ) , j - 1 }
162+ build_diff ( matrix , x , y , i , j - 1 , edits ++ [ newedit ] , options )
121163 i > 0 and ( j == 0 or Matrix . get ( matrix , i , j - 1 ) < Matrix . get ( matrix , i - 1 , j ) ) ->
122- build_diff ( matrix , x , y , i - 1 , j , edits ++ [ { :delete , Enum . fetch! ( x , i - 1 ) , i - 1 } ] , options )
164+ newdelete = { :delete , Enum . fetch! ( x , i - 1 ) , j }
165+ build_diff ( matrix , x , y , i - 1 , j , edits ++ [ newdelete ] , options )
123166 true ->
124167 edits |> Enum . reverse
125168 end
126169 end
127170
128171 defp build_changes ( edits , options ) do
129- Enum . reduce ( edits , [ ] , fn ( { type , char , index } , changes ) ->
172+
173+ # we now have a set of individual letter changes
174+ # but if there is a series of inserts or deletes then
175+ # we need to reduce them into single multichar changes
176+ mergeindividualchangesFn = fn ( { type , char , index } , changes ) ->
130177 if changes == [ ] do
131- changes ++ [ change ( type , char , index ) ]
178+ changes ++ [ make_change ( type , char , index ) ]
132179 else
133180 change = List . last ( changes )
134181 regex = Dict . get ( options , :ignore )
135-
136182 cond do
137183 regex && Regex . match? ( regex , char ) ->
138- changes ++ [ change ( :ignored , char , index ) ]
139- is_type ( change , type ) && index == ( change . index + change . length ) ->
140- change = % { change | element: change . element ++ [ char ] , length: change . length + 1 }
141-
142- if regex && Regex . match? ( regex , Enum . join ( change . element ) ) do
143- change = % Ignored { element: change . element , index: change . index , length: change . length }
144- end
145-
184+ changes ++ [ make_change ( :ignored , char , index ) ]
185+ # one branch for deletes
186+ is_type ( change , type ) && type == :delete && index == change . index ->
187+ change = if regex && Regex . match? ( regex , Enum . join ( change . element ) ) do
188+ % Ignored { element: change . element , index: change . index ,
189+ length: change . length }
190+ else
191+ % { change | element: change . element ++ [ char ] , length:
192+ change . length + 1 }
193+ end
194+ List . replace_at ( changes , length ( changes ) - 1 , change )
195+ # a different branch for everyone else
196+ is_type ( change , type ) && type != :delete && index == ( change . index + change . length ) ->
197+ change = if regex && Regex . match? ( regex , Enum . join ( change . element ) ) do
198+ % Ignored { element: change . element , index: change . index ,
199+ length: change . length }
200+ else
201+ % { change | element: change . element ++ [ char ] , length:
202+ change . length + 1 }
203+ end
146204 List . replace_at ( changes , length ( changes ) - 1 , change )
147205 true ->
148- changes ++ [ change ( type , char , index ) ]
149-
206+ changes ++ [ make_change ( type , char , index ) ]
150207 end
151208 end
209+ end
152210
153-
154- end )
155-
156- |> Enum . reduce ( [ ] , fn ( x , changes ) ->
211+ # if we change a single letter it will be a consecutive delete/insert
212+ # this reduction merges them into a single modified statement
213+ makemodifiedFn = fn ( x , changes ) ->
157214 if changes == [ ] do
158215 [ x ]
159216 else
160217 last_change = List . last ( changes )
161-
162- if is_type ( last_change , :delete ) and is_type ( x , :insert ) and last_change . index == x . index and last_change . length == x . length do
163- last_change = % Modified { element: x . element , old_element: last_change . element , index: x . index , length: x . length }
218+ if is_type ( last_change , :delete )
219+ and is_type ( x , :insert )
220+ and last_change . index == x . index
221+ and last_change . length == x . length do
222+ last_change = % Modified { element: x . element , old_element: last_change . element ,
223+ index: x . index , length: x . length }
164224 List . replace_at ( changes , length ( changes ) - 1 , last_change )
165225 else
166226 changes ++ [ x ]
167227 end
168228 end
169- end )
170- end
229+ end
171230
231+ # Now do both these sets of reduction on the edits
232+ Enum . reduce ( edits , [ ] , mergeindividualchangesFn )
233+ |> Enum . reduce ( [ ] , makemodifiedFn )
234+ end
172235
173- defp change ( :insert , char , index ) do
236+ defp make_change ( :insert , char , index ) do
174237 % Insert { element: [ char ] , index: index , length: 1 }
175238 end
176239
177- defp change ( :delete , char , index ) do
240+ defp make_change ( :delete , char , index ) do
178241 % Delete { element: [ char ] , index: index , length: 1 }
179242 end
180243
181- defp change ( :unchanged , char , index ) do
244+ defp make_change ( :unchanged , char , index ) do
182245 % Unchanged { element: [ char ] , index: index , length: 1 }
183246 end
184247
185- defp change ( :ignored , char , index ) do
248+ defp make_change ( :ignored , char , index ) do
186249 % Ignored { element: [ char ] , index: index , length: 1 }
187250 end
188251
@@ -206,4 +269,23 @@ defmodule Diff do
206269 false
207270 end
208271
272+ defp annotate ( list , type , annotations , increment ) do
273+ annotation = for a <- annotations ,
274+ Map . get ( a , type ) != nil , do: Map . get ( a , type )
275+ case { type , annotation } do
276+ { :deleted , [ ] } -> { [ ] , increment }
277+ { _ , [ ] } -> { list , increment }
278+ { :deleted , [ annotation ] } -> apply_deletion ( list , annotation , increment )
279+ { _ , [ annotation ] } -> apply_annotation ( list , annotation , increment )
280+ end
281+ end
282+
283+ defp apply_deletion ( list , annotation , increment ) do
284+ { [ annotation . before ] ++ list ++ [ annotation . after ] , increment + 2 }
285+ end
286+
287+ defp apply_annotation ( list , annotation , increment ) do
288+ { [ annotation . before ] ++ list ++ [ annotation . after ] , increment + 2 }
289+ end
290+
209291end
0 commit comments