lib/philtre/block/code.ex

defmodule Philtre.Block.Code do
  @moduledoc """
  Elixir-side implementation of the code-type block

  This block is used to write code in a synthax-highlighted UI. The frontend
  aspect of it is implemented in `hooks/Code.ts`
  """
  use Phoenix.Component

  alias Philtre.Block
  alias Philtre.Block.ContentEditable
  alias Philtre.Block.ContentEditable.Selection
  alias Philtre.Editor
  alias Philtre.Editor.Utils

  require Logger

  @behaviour Block

  defstruct id: nil, content: "", language: "elixir", focused: false

  @type t :: %__MODULE__{}

  @impl Block
  def id(%__MODULE__{id: id}), do: id

  @impl Block
  def type(%__MODULE__{}), do: "code"

  @impl Block
  def data(%__MODULE__{language: language, content: content}) do
    %{"language" => language, "content" => content}
  end

  @impl Block
  def normalize(id, %{"language" => language, "content" => content}) do
    %__MODULE__{
      id: id,
      language: language,
      content: content
    }
  end

  @impl Block
  def render_live(%{block: _} = assigns) do
    # data-language is used to get the language in the frontend hook, which is
    # then used by the frontend-based code-highlighting library
    ~H"""
    <div
      class="philtre__code"
      data-block
      data-language={@block.language}
      id={@block.id}
      phx-hook="Code"
      phx-update="ignore"
      phx-target={@myself}
    >
      <.language language={@block.language} myself={@myself} />
      <pre><code class="philtre__code__highlighted"><%= @block.content %></code></pre>
      <textarea
        class="philtre__code__editable"
        spellcheck="false"
        autofocus={@block.focused}
        rows={rows(@block.content)}><%= @block.content %></textarea>
    </div>
    """
  end

  defp language(%{} = assigns) do
    ~H"""
    <div class="philtre__code__language">
      <form phx-change="set_language" phx-target={@myself}>
        <select name="language" value={@language}>
          <option value="elixir">Elixir</option>
          <option value="javascript">Javascript</option>
        </select>
      </form>
    </div>
    """
  end

  defp rows(content) when is_binary(content) do
    content
    |> String.split("\n")
    |> Enum.count()
  end

  @impl Block
  def render_static(%{block: _} = assigns) do
    ~H"<pre><%= @block.content %></pre>"
  end

  def handle_event("update", %{"value" => value}, socket) do
    new_block = %{socket.assigns.block | content: value}
    new_editor = Editor.replace_block(socket.assigns.editor, socket.assigns.block, [new_block])

    send(self(), {:update, new_editor})

    {:noreply, socket}
  end

  def handle_event("add_block", %{} = params, socket) do
    Logger.debug("add_block: #{inspect(params)}")

    %__MODULE__{} = block = socket.assigns.block

    cell = ContentEditable.Cell.new()

    new_block = %ContentEditable{
      id: Utils.new_id(),
      kind: "p",
      cells: [cell],
      selection: ContentEditable.Selection.new_start_of(cell)
    }

    new_editor = Editor.replace_block(socket.assigns.editor, block, [block, new_block])

    send(self(), {:update, new_editor})

    {:noreply, socket}
  end

  def handle_event("set_language", %{"language" => language} = params, socket) do
    Logger.debug("set_block: #{inspect(params)}")

    new_block = %{socket.assigns.block | language: language}
    new_editor = Editor.replace_block(socket.assigns.editor, socket.assigns.block, [new_block])

    send(self(), {:update, new_editor})

    {:noreply, socket}
  end

  @impl Block
  @spec transform(ContentEditable.t()) :: t
  def transform(%ContentEditable{} = _block) do
    %__MODULE__{id: Utils.new_id(), content: "", language: "elixir", focused: true}
  end

  @impl Block
  def reduce(%__MODULE__{} = block), do: block

  @impl Block
  def set_selection(%__MODULE__{} = block, %Selection{}) do
    # We do not set selection on code block from BE yet
    block
  end

  @impl Block
  def cells(%__MODULE__{}), do: []
end