@@ -121,7 +121,11 @@ defmodule Livebook.Session.DataSync do
121121 defp key ( % Cell.Code { } = cell ) ,
122122 do: { :code_cell , cell . source , Map . take ( cell , cell_syncable_fields ( cell ) ) }
123123
124- defp key ( % Cell.Smart { } = cell ) , do: { :smart_cell , cell . source , cell . attrs }
124+ # Note that smart cell attributes in memory may be different than
125+ # onces after notebook export end import (e.g. atoms are stringified).
126+ # We could encode them here, but source should be enough as a key,
127+ # since attrs generally determine the source.
128+ defp key ( % Cell.Smart { } = cell ) , do: { :smart_cell , cell . source }
125129
126130 defp cell_syncable_fields ( % Cell.Code { } ) ,
127131 do: [ :language , :reevaluate_automatically , :continue_on_error ]
@@ -200,14 +204,52 @@ defmodule Livebook.Session.DataSync do
200204
201205 defp do_annotate_moves ( [ ] , _ops_map ) , do: [ ]
202206
203- defp annotate_updates ( [ { :del , key1 , item1 } , { :ins , key2 , item2 } | ops ] )
207+ defp annotate_updates ( ops ) do
208+ # We could simply match on consecutive :del and :ins, however the
209+ # diff may include a sequence of :del ops and then a sequence of
210+ # :ins ops. They may not match in a zip way either, for example,
211+ # the first :del may not match the insert, but the second :del
212+ # does, so we should keep the first :del and make thse second one
213+ # into an :upd.
214+ #
215+ # To do this, we go through the ops and accumulate consecutive
216+ # :del ops, then once we get to an :ins, we traverse those :del
217+ # ops in the same order, looking for a matching one. If a :del
218+ # is matching, we flush an :upd and continue going thorugh ops.
219+ # Otherwise, we flush the non-matching :del as is and try the
220+ # next :del in the sequence. Finally, if there is no matching
221+ # :del, we flush :ins as is and continue going through the ops.
222+ do_annotate_updates ( ops , [ ] , [ ] )
223+ end
224+
225+ defp do_annotate_updates ( [ { :del , key , item } | ops ] , pending_dels , acc ) do
226+ do_annotate_updates ( ops , [ { :del , key , item } | pending_dels ] , acc )
227+ end
228+
229+ defp do_annotate_updates ( [ { :ins , key , item } | ops ] , pending_dels , acc ) do
230+ find_matching_del ( Enum . reverse ( pending_dels ) , { :ins , key , item } , ops , acc )
231+ end
232+
233+ defp do_annotate_updates ( [ op | ops ] , pending_dels , acc ) do
234+ do_annotate_updates ( ops , [ ] , [ op | pending_dels ++ acc ] )
235+ end
236+
237+ defp do_annotate_updates ( [ ] , pending_dels , acc ) do
238+ Enum . reverse ( pending_dels ++ acc )
239+ end
240+
241+ defp find_matching_del ( [ { :del , key1 , item1 } | dels ] , { :ins , key2 , item2 } , ops , acc )
204242 when elem ( key1 , 0 ) == elem ( key2 , 0 ) do
205- # If the same block type, then we change it into an update.
206- [ { :upd , key1 , key2 , item1 , item2 } | annotate_updates ( ops ) ]
243+ do_annotate_updates ( ops , Enum . reverse ( dels ) , [ { :upd , key1 , key2 , item1 , item2 } | acc ] )
207244 end
208245
209- defp annotate_updates ( [ op | ops ] ) , do: [ op | annotate_updates ( ops ) ]
210- defp annotate_updates ( [ ] ) , do: [ ]
246+ defp find_matching_del ( [ del | dels ] , ins , ops , acc ) do
247+ find_matching_del ( dels , ins , ops , [ del | acc ] )
248+ end
249+
250+ defp find_matching_del ( [ ] , ins , ops , acc ) do
251+ do_annotate_updates ( ops , [ ] , [ ins | acc ] )
252+ end
211253
212254 defp diff_to_data_ops ( diff , data , client_id ) do
213255 acc = % {
0 commit comments