Skip to content

Commit 89c380e

Browse files
committed
Fix some smart cells getting cleared on notebook sync (#3161)
1 parent 9d52c02 commit 89c380e

File tree

2 files changed

+93
-9
lines changed

2 files changed

+93
-9
lines changed

lib/livebook/session/data_sync.ex

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -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 = %{

test/livebook/session/data_sync_test.exs

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -203,18 +203,20 @@ defmodule Livebook.Session.DataSyncTest do
203203
Notebook.new()
204204
| sections: [
205205
%{Section.new() | id: "s1", name: "Section 1"},
206-
%{Section.new() | id: "s2", name: "Section 2", parent_id: "s1"}
206+
%{Section.new() | id: "s2", name: "Section 2"},
207+
%{Section.new() | id: "s3", name: "Section 3", parent_id: "s1"}
207208
]
208209
}
209210

210211
after_notebook = %{
211212
Notebook.new()
212213
| sections: [
213-
%{Section.new() | id: "a-s2", name: "Section 2"}
214+
%{Section.new() | id: "a-s2", name: "Section 2"},
215+
%{Section.new() | id: "a-s3", name: "Section 3"}
214216
]
215217
}
216218

217-
expected_ops = [{:unset_section_parent, @cid, "s2"}, {:delete_section, @cid, "s1", false}]
219+
expected_ops = [{:unset_section_parent, @cid, "s3"}, {:delete_section, @cid, "s1", false}]
218220
assert_ops_and_result(before_notebook, after_notebook, expected_ops)
219221
end
220222

@@ -361,6 +363,46 @@ defmodule Livebook.Session.DataSyncTest do
361363
assert_ops_and_result(before_notebook, after_notebook, expected_ops)
362364
end
363365

366+
test "consecutive code cells source updated" do
367+
before_notebook = %{
368+
Notebook.new()
369+
| sections: [
370+
%{
371+
Section.new()
372+
| id: "s1",
373+
cells: [
374+
%{Cell.new(:code) | id: "c1", source: "x = 1"},
375+
%{Cell.new(:code) | id: "c2", source: "y = 2"}
376+
]
377+
}
378+
]
379+
}
380+
381+
after_notebook = %{
382+
Notebook.new()
383+
| sections: [
384+
%{
385+
Section.new()
386+
| id: "a-s1",
387+
cells: [
388+
%{Cell.new(:code) | id: "a-c1", source: "x = 2"},
389+
%{Cell.new(:code) | id: "a-c2", source: "y = 3"}
390+
]
391+
}
392+
]
393+
}
394+
395+
delta1 = Delta.diff("x = 1", "x = 2")
396+
delta2 = Delta.diff("y = 2", "y = 3")
397+
398+
expected_ops = [
399+
{:apply_cell_delta, @cid, "c1", :primary, delta1, nil, 0},
400+
{:apply_cell_delta, @cid, "c2", :primary, delta2, nil, 0}
401+
]
402+
403+
assert_ops_and_result(before_notebook, after_notebook, expected_ops)
404+
end
405+
364406
test "code cell attrs updated" do
365407
before_notebook = %{
366408
Notebook.new()

0 commit comments

Comments
 (0)