<% end %>
<%= live_patch to: Routes.session_path(@socket, :file, @session_id) do %>
<%= if @data.path do %>
<%= if @data.dirty do %>
<%= Icons.svg(:dots_circle_horizontal, class: "h-5 text-blue-400") %>
<% else %>
<%= Icons.svg(:check_circle, class: "h-5 text-green-400") %>
<% end %>
<%= Path.basename(@data.path) %>
<% else %>
<%= Icons.svg(:document_text, class: "h-5 text-gray-400") %>
No file choosen
<% end %>
<% end %>
<%= live_patch to: Routes.home_path(@socket, :page) do %>
<%= Icons.svg(:home, class: "h-6 w-6 text-gray-600 hover:text-current") %>
<% end %>
<%= live_patch to: Routes.session_path(@socket, :shortcuts, @session_id) do %>
<%= Icons.svg(:question_mark_circle, class: "h-6 w-6 text-gray-600 hover:text-current") %>
<% end %>
<%= for section <- @data.notebook.sections do %>
<%= live_component @socket, LiveBookWeb.SectionComponent,
id: section.id,
section: section,
selected: section.id == @selected_section_id,
cell_infos: @data.cell_infos,
focused_cell_id: @focused_cell_id,
insert_mode: @insert_mode %>
<% end %>
<%= if @insert_mode do %>
<%# Show a tiny insert indicator for clarity %>
insert
<% end %>
"""
end
@impl true
def handle_params(_params, _url, socket) do
{:noreply, socket}
end
@impl true
def handle_event("cell_init", %{"cell_id" => cell_id}, socket) do
data = socket.assigns.data
case Notebook.fetch_cell_and_section(data.notebook, cell_id) do
{:ok, cell, _section} ->
payload = %{
source: cell.source,
revision: data.cell_infos[cell.id].revision
}
{:reply, payload, socket}
:error ->
{:noreply, socket}
end
end
def handle_event("add_section", _params, socket) do
end_index = length(socket.assigns.data.notebook.sections)
Session.insert_section(socket.assigns.session_id, end_index)
{:noreply, socket}
end
def handle_event("delete_section", %{"section_id" => section_id}, socket) do
Session.delete_section(socket.assigns.session_id, section_id)
{:noreply, socket}
end
def handle_event("select_section", %{"section_id" => section_id}, socket) do
{:noreply, assign(socket, selected_section_id: section_id)}
end
def handle_event(
"insert_cell",
%{"section_id" => section_id, "index" => index, "type" => type},
socket
) do
index = String.to_integer(index) |> max(0)
type = String.to_atom(type)
Session.insert_cell(socket.assigns.session_id, section_id, index, type)
{:noreply, socket}
end
def handle_event("insert_cell_below_focused", %{"type" => type}, socket) do
type = String.to_atom(type)
insert_cell_next_to_focused(socket.assigns, type, idx_offset: 1)
{:noreply, socket}
end
def handle_event("insert_cell_above_focused", %{"type" => type}, socket) do
type = String.to_atom(type)
insert_cell_next_to_focused(socket.assigns, type, idx_offset: 0)
{:noreply, socket}
end
def handle_event("delete_focused_cell", %{}, socket) do
if socket.assigns.focused_cell_id do
Session.delete_cell(socket.assigns.session_id, socket.assigns.focused_cell_id)
end
{:noreply, socket}
end
def handle_event("set_notebook_name", %{"name" => name}, socket) do
name = normalize_name(name)
Session.set_notebook_name(socket.assigns.session_id, name)
{:noreply, socket}
end
def handle_event("set_section_name", %{"section_id" => section_id, "name" => name}, socket) do
name = normalize_name(name)
Session.set_section_name(socket.assigns.session_id, section_id, name)
{:noreply, socket}
end
def handle_event(
"apply_cell_delta",
%{"cell_id" => cell_id, "delta" => delta, "revision" => revision},
socket
) do
delta = Delta.from_compressed(delta)
Session.apply_cell_delta(socket.assigns.session_id, self(), cell_id, delta, revision)
{:noreply, socket}
end
def handle_event(
"report_cell_revision",
%{"cell_id" => cell_id, "revision" => revision},
socket
) do
Session.report_cell_revision(socket.assigns.session_id, self(), cell_id, revision)
{:noreply, socket}
end
def handle_event("focus_cell", %{"cell_id" => nil}, socket) do
{:noreply, focus_cell(socket, nil)}
end
def handle_event("focus_cell", %{"cell_id" => cell_id}, socket) do
case Notebook.fetch_cell_and_section(socket.assigns.data.notebook, cell_id) do
{:ok, cell, _section} ->
{:noreply, focus_cell(socket, cell)}
:error ->
{:noreply, socket}
end
end
def handle_event("move_cell_focus", %{"offset" => offset}, socket) do
case new_focused_cell_from_offset(socket.assigns, offset) do
{:ok, cell} ->
{:noreply, focus_cell(socket, cell)}
:error ->
{:noreply, socket}
end
end
def handle_event("move_cell", %{"offset" => offset}, socket) do
if socket.assigns.focused_cell_id do
Session.move_cell(socket.assigns.session_id, socket.assigns.focused_cell_id, offset)
end
{:noreply, socket}
end
def handle_event("set_insert_mode", %{"enabled" => enabled}, socket) do
if socket.assigns.focused_cell_id do
{:noreply, assign(socket, insert_mode: enabled)}
else
{:noreply, socket}
end
end
def handle_event("enable_insert_mode", %{}, socket) do
if socket.assigns.focused_cell_id do
{:noreply, assign(socket, insert_mode: true)}
else
{:noreply, socket}
end
end
def handle_event("queue_focused_cell_evaluation", %{}, socket) do
if socket.assigns.focused_cell_id do
Session.queue_cell_evaluation(socket.assigns.session_id, socket.assigns.focused_cell_id)
end
{:noreply, socket}
end
def handle_event("queue_section_cells_evaluation", %{}, socket) do
if socket.assigns.selected_section_id do
{:ok, section} =
Notebook.fetch_section(
socket.assigns.data.notebook,
socket.assigns.selected_section_id
)
for cell <- section.cells, cell.type == :elixir do
Session.queue_cell_evaluation(socket.assigns.session_id, cell.id)
end
end
{:noreply, socket}
end
def handle_event("queue_child_cells_evaluation", %{}, socket) do
if socket.assigns.focused_cell_id do
{:ok, cell, _section} =
Notebook.fetch_cell_and_section(
socket.assigns.data.notebook,
socket.assigns.focused_cell_id
)
cells = Notebook.child_cells(socket.assigns.data.notebook, cell.id)
for cell <- cells, cell.type == :elixir do
Session.queue_cell_evaluation(socket.assigns.session_id, cell.id)
end
end
{:noreply, socket}
end
def handle_event("cancel_focused_cell_evaluation", %{}, socket) do
if socket.assigns.focused_cell_id do
Session.cancel_cell_evaluation(socket.assigns.session_id, socket.assigns.focused_cell_id)
end
{:noreply, socket}
end
def handle_event("show_shortcuts", %{}, socket) do
{:noreply,
push_patch(socket, to: Routes.session_path(socket, :shortcuts, socket.assigns.session_id))}
end
@impl true
def handle_info({:operation, operation}, socket) do
case Session.Data.apply_operation(socket.assigns.data, operation) do
{:ok, data, actions} ->
new_socket =
socket
|> assign(data: data)
|> after_operation(socket, operation)
|> handle_actions(actions)
{:noreply, new_socket}
:error ->
{:noreply, socket}
end
end
def handle_info({:error, error}, socket) do
message = error |> to_string() |> String.capitalize()
{:noreply, put_flash(socket, :error, message)}
end
def handle_info({:info, info}, socket) do
message = info |> to_string() |> String.capitalize()
{:noreply, put_flash(socket, :info, message)}
end
def handle_info(:session_closed, socket) do
{:noreply,
socket
|> put_flash(:info, "Session has been closed")
|> push_redirect(to: Routes.home_path(socket, :page))}
end
def handle_info(_message, socket), do: {:noreply, socket}
defp after_operation(socket, _prev_socket, {:insert_section, _index, section_id}) do
assign(socket, selected_section_id: section_id)
end
defp after_operation(socket, _prev_socket, {:delete_section, _section_id}) do
assign(socket, selected_section_id: nil)
end
defp after_operation(socket, _prev_socket, {:insert_cell, _, _, _, cell_id}) do
{:ok, cell, _section} = Notebook.fetch_cell_and_section(socket.assigns.data.notebook, cell_id)
focus_cell(socket, cell, insert_mode: true)
end
defp after_operation(socket, prev_socket, {:delete_cell, cell_id}) do
if cell_id == socket.assigns.focused_cell_id do
case Notebook.fetch_cell_sibling(prev_socket.assigns.data.notebook, cell_id, 1) do
{:ok, next_cell} ->
focus_cell(socket, next_cell)
:error ->
case Notebook.fetch_cell_sibling(prev_socket.assigns.data.notebook, cell_id, -1) do
{:ok, previous_cell} ->
focus_cell(socket, previous_cell)
:error ->
focus_cell(socket, nil)
end
end
else
socket
end
end
defp after_operation(socket, _prev_socket, _operation), do: socket
defp handle_actions(socket, actions) do
Enum.reduce(actions, socket, &handle_action(&2, &1))
end
defp handle_action(socket, {:broadcast_delta, from, cell, delta}) do
if from == self() do
push_event(socket, "cell_acknowledgement:#{cell.id}", %{})
else
push_event(socket, "cell_delta:#{cell.id}", %{delta: Delta.to_compressed(delta)})
end
end
defp handle_action(socket, _action), do: socket
defp normalize_name(name) do
name
|> String.trim()
|> String.replace(~r/\s+/, " ")
|> case do
"" -> "Untitled"
name -> name
end
end
defp focus_cell(socket, cell, opts \\ [])
defp focus_cell(socket, nil = _cell, _opts) do
assign(socket, focused_cell_id: nil, focused_cell_type: nil, insert_mode: false)
end
defp focus_cell(socket, cell, opts) do
insert_mode? = Keyword.get(opts, :insert_mode, false)
assign(socket,
focused_cell_id: cell.id,
focused_cell_type: cell.type,
insert_mode: insert_mode?
)
end
defp insert_cell_next_to_focused(assigns, type, idx_offset: idx_offset) do
if assigns.focused_cell_id do
{:ok, cell, section} =
Notebook.fetch_cell_and_section(assigns.data.notebook, assigns.focused_cell_id)
index = Enum.find_index(section.cells, &(&1 == cell))
Session.insert_cell(assigns.session_id, section.id, index + idx_offset, type)
else
append_cell_to_section(assigns, type)
end
end
defp append_cell_to_section(assigns, type) do
if assigns.selected_section_id do
{:ok, section} = Notebook.fetch_section(assigns.data.notebook, assigns.selected_section_id)
end_index = length(section.cells)
Session.insert_cell(assigns.session_id, section.id, end_index, type)
end
end
defp new_focused_cell_from_offset(assigns, offset) do
cond do
assigns.focused_cell_id ->
# If a cell is focused, look up the appropriate sibling
Notebook.fetch_cell_sibling(assigns.data.notebook, assigns.focused_cell_id, offset)
assigns.selected_section_id ->
# If no cell is focused, focus the first one for easier keyboard navigation.
{:ok, section} =
Notebook.fetch_section(assigns.data.notebook, assigns.selected_section_id)
Enum.fetch(section.cells, 0)
true ->
:error
end
end
defp runtime_description(nil), do: "No runtime"
defp runtime_description(%Runtime.ElixirStandalone{}), do: "Elixir standalone runtime"
defp runtime_description(%Runtime.MixStandalone{}), do: "Mix standalone runtime"
defp runtime_description(%Runtime.Attached{}), do: "Attached runtime"
end