diff --git a/assets/js/session/index.js b/assets/js/session/index.js
index 57b5a2277..a8e4c954b 100644
--- a/assets/js/session/index.js
+++ b/assets/js/session/index.js
@@ -89,6 +89,10 @@ const Session = {
handleSectionDeleted(this, sectionId);
});
+ this.handleEvent("section_moved", ({ section_id: sectionId }) => {
+ handleSectionMoved(this, sectionId);
+ });
+
this.handleEvent("cell_upload", ({ cell_id: cellId, url }) => {
handleCellUpload(this, cellId, url);
});
@@ -504,6 +508,12 @@ function handleSectionDeleted(hook, sectionId) {
}
}
+function handleSectionMoved(hook, sectionId) {
+ if (hook.state.focusedSectionId === sectionId) {
+ globalPubSub.broadcast("session", { type: "section_moved", sectionId });
+ }
+}
+
function handleCellUpload(hook, cellId, url) {
if (hook.state.focusedCellId !== cellId) {
setFocusedCell(hook, cellId);
diff --git a/lib/livebook/notebook.ex b/lib/livebook/notebook.ex
index 792f19087..1d2d8cc82 100644
--- a/lib/livebook/notebook.ex
+++ b/lib/livebook/notebook.ex
@@ -229,6 +229,28 @@ defmodule Livebook.Notebook do
index |> max(0) |> min(length(list) - 1)
end
+ @doc """
+ Moves section by the given offset.
+ """
+ @spec move_section(t(), Section.id(), integer()) :: t()
+ def move_section(notebook, section_id, offset) do
+ # We first find the index of the given section.
+ # Then we find its' new index from given offset.
+ # Finally, we move the section, and return the new notebook.
+
+ idx =
+ Enum.find_index(notebook.sections, fn
+ section -> section.id == section_id
+ end)
+
+ new_idx = (idx + offset) |> clamp_index(notebook.sections)
+
+ {section, sections} = List.pop_at(notebook.sections, idx)
+ sections = List.insert_at(sections, new_idx, section)
+
+ %{notebook | sections: sections}
+ end
+
@doc """
Returns a list of `{cell, section}` pairs including all Elixir cells in order.
"""
diff --git a/lib/livebook/session.ex b/lib/livebook/session.ex
index 117f9b1f9..bc3c112d1 100644
--- a/lib/livebook/session.ex
+++ b/lib/livebook/session.ex
@@ -144,6 +144,14 @@ defmodule Livebook.Session do
GenServer.cast(name(session_id), {:move_cell, self(), cell_id, offset})
end
+ @doc """
+ Asynchronously sends section move request to the server.
+ """
+ @spec move_section(id(), Section.id(), integer()) :: :ok
+ def move_section(session_id, section_id, offset) do
+ GenServer.cast(name(session_id), {:move_section, self(), section_id, offset})
+ end
+
@doc """
Asynchronously sends cell evaluation request to the server.
"""
@@ -357,6 +365,11 @@ defmodule Livebook.Session do
{:noreply, handle_operation(state, operation)}
end
+ def handle_cast({:move_section, client_pid, section_id, offset}, state) do
+ operation = {:move_section, client_pid, section_id, offset}
+ {:noreply, handle_operation(state, operation)}
+ end
+
def handle_cast({:queue_cell_evaluation, client_pid, cell_id}, state) do
case ensure_runtime(state) do
{:ok, state} ->
diff --git a/lib/livebook/session/data.ex b/lib/livebook/session/data.ex
index 776b956b8..f251aeb75 100644
--- a/lib/livebook/session/data.ex
+++ b/lib/livebook/session/data.ex
@@ -77,6 +77,7 @@ defmodule Livebook.Session.Data do
| {:delete_section, pid(), Section.id()}
| {:delete_cell, pid(), Cell.id()}
| {:move_cell, pid(), Cell.id(), offset :: integer()}
+ | {:move_section, pid(), Section.id(), offset :: integer()}
| {:queue_cell_evaluation, pid(), Cell.id()}
| {:add_cell_evaluation_stdout, pid(), Cell.id(), String.t()}
| {:add_cell_evaluation_response, pid(), Cell.id(), Evaluator.evaluation_response()}
@@ -229,6 +230,19 @@ defmodule Livebook.Session.Data do
end
end
+ def apply_operation(data, {:move_section, _client_pid, id, offset}) do
+ with {:ok, section} <- Notebook.fetch_section(data.notebook, id),
+ true <- offset != 0 do
+ data
+ |> with_actions()
+ |> move_section(section, offset)
+ |> set_dirty()
+ |> wrap_ok()
+ else
+ _ -> :error
+ end
+ end
+
def apply_operation(data, {:queue_cell_evaluation, _client_pid, id}) do
with {:ok, cell, section} <- Notebook.fetch_cell_and_section(data.notebook, id),
:elixir <- cell.type,
@@ -455,8 +469,22 @@ defmodule Livebook.Session.Data do
defp move_cell({data, _} = data_actions, cell, offset) do
updated_notebook = Notebook.move_cell(data.notebook, cell.id, offset)
- cells_with_section_before = Notebook.elixir_cells_with_section(data.notebook)
- cells_with_section_after = Notebook.elixir_cells_with_section(updated_notebook)
+ data_actions
+ |> set!(notebook: updated_notebook)
+ |> update_cells_status_after_moved(data.notebook)
+ end
+
+ defp move_section({data, _} = data_actions, section, offset) do
+ updated_notebook = Notebook.move_section(data.notebook, section.id, offset)
+
+ data_actions
+ |> set!(notebook: updated_notebook)
+ |> update_cells_status_after_moved(data.notebook)
+ end
+
+ defp update_cells_status_after_moved({data, _} = data_actions, prev_notebook) do
+ cells_with_section_before = Notebook.elixir_cells_with_section(prev_notebook)
+ cells_with_section_after = Notebook.elixir_cells_with_section(data.notebook)
affected_cells_with_section =
cells_with_section_after
@@ -467,7 +495,6 @@ defmodule Livebook.Session.Data do
|> Enum.map(fn {new, _old} -> new end)
data_actions
- |> set!(notebook: updated_notebook)
|> mark_cells_as_stale(affected_cells_with_section)
|> unqueue_cells_evaluation(affected_cells_with_section)
end
diff --git a/lib/livebook_web/live/session_live.ex b/lib/livebook_web/live/session_live.ex
index 3a76496bd..69a6bb497 100644
--- a/lib/livebook_web/live/session_live.ex
+++ b/lib/livebook_web/live/session_live.ex
@@ -300,6 +300,13 @@ defmodule LivebookWeb.SessionLive do
{:noreply, socket}
end
+ def handle_event("move_section", %{"section_id" => section_id, "offset" => offset}, socket) do
+ offset = ensure_integer(offset)
+ Session.move_section(socket.assigns.session_id, section_id, offset)
+
+ {:noreply, socket}
+ end
+
def handle_event("queue_cell_evaluation", %{"cell_id" => cell_id}, socket) do
Session.queue_cell_evaluation(socket.assigns.session_id, cell_id)
{:noreply, socket}
diff --git a/lib/livebook_web/live/session_live/section_component.ex b/lib/livebook_web/live/session_live/section_component.ex
index e774e9616..3d972c87c 100644
--- a/lib/livebook_web/live/session_live/section_component.ex
+++ b/lib/livebook_web/live/session_live/section_component.ex
@@ -17,6 +17,22 @@ defmodule LivebookWeb.SessionLive.SectionComponent do
<%# ^ Note it's important there's no space between
and
because we want the content to exactly match section name. %>
+
+
+
+
+
+