igniter/lib/code/list.ex
2024-06-12 18:22:08 -04:00

155 lines
3.9 KiB
Elixir

defmodule Igniter.Code.List do
@moduledoc """
Utilities for working with lists.
"""
require Igniter.Code.Common
alias Igniter.Code.Common
alias Sourceror.Zipper
@type equality_pred :: (Zipper.t(), Macro.t() -> boolean)
@doc "Returns true if the `zipper` is at a list literal"
@spec list?(Zipper.t()) :: boolean()
def list?(zipper) do
Common.node_matches_pattern?(zipper, value when is_list(value))
end
@doc "Prepends `quoted` to the list unless it is alread present, determined by `equality_pred`."
@spec prepend_new_to_list(Zipper.t(), quoted :: Macro.t(), equality_pred) ::
{:ok, Zipper.t()} | :error
def prepend_new_to_list(zipper, quoted, equality_pred \\ &Common.nodes_equal?/2) do
if list?(zipper) do
zipper
|> find_list_item_index(fn value ->
equality_pred.(value, quoted)
end)
|> case do
nil ->
prepend_to_list(zipper, quoted)
_ ->
{:ok, zipper}
end
else
:error
end
end
@doc "Appends `quoted` to the list unless it is alread present, determined by `equality_pred`."
@spec append_new_to_list(Zipper.t(), quoted :: Macro.t(), equality_pred) ::
{:ok, Zipper.t()} | :error
def append_new_to_list(zipper, quoted, equality_pred \\ &Common.nodes_equal?/2) do
if list?(zipper) do
zipper
|> find_list_item_index(fn value ->
equality_pred.(value, quoted)
end)
|> case do
nil ->
zipper
|> Common.maybe_move_to_block()
|> Zipper.append_child(quoted)
_ ->
zipper
end
else
:error
end
end
@doc "Prepends `quoted` to the list"
@spec prepend_to_list(Zipper.t(), quoted :: Macro.t()) :: {:ok, Zipper.t()} | :error
def prepend_to_list(zipper, quoted) do
if list?(zipper) do
{:ok,
zipper
|> Common.maybe_move_to_block()
|> Zipper.insert_child(quoted)}
else
:error
end
end
@doc "Appends `quoted` to the list"
@spec append_to_list(Zipper.t(), quoted :: Macro.t()) :: {:ok, Zipper.t()} | :error
def append_to_list(zipper, quoted) do
if list?(zipper) do
{:ok,
zipper
|> Common.maybe_move_to_block()
|> Zipper.append_child(quoted)}
else
:error
end
end
@doc "Removes the item at the given index, returning `:error` if nothing is at that index"
@spec append_to_list(Zipper.t(), quoted :: Macro.t()) :: {:ok, Zipper.t()} | :error
def remove_index(zipper, index) do
if list?(zipper) do
Common.within(zipper, fn zipper ->
zipper
|> Zipper.down()
|> Common.nth_right(index)
|> case do
:error ->
:error
{:ok, zipper} ->
{:ok, Zipper.remove(zipper)}
end
end)
else
:error
end
end
@doc "Finds the index of the first list item that satisfies `pred`"
@spec find_list_item_index(Zipper.t(), (Macro.t() -> boolean())) :: integer() | nil
def find_list_item_index(zipper, pred) do
# go into first list item
zipper
|> Common.maybe_move_to_block()
|> Zipper.down()
|> case do
nil ->
nil
zipper ->
find_index_right(zipper, pred, 0)
end
end
@doc "Moves to the list item matching the given predicate"
@spec move_to_list_item(Zipper.t(), (Macro.t() -> boolean())) :: {:ok, Zipper.t()} | :error
def move_to_list_item(zipper, pred) do
# go into first list item
zipper
|> Common.maybe_move_to_block()
|> Zipper.down()
|> case do
nil ->
:error
zipper ->
Common.move_right(zipper, pred)
end
end
defp find_index_right(zipper, pred, index) do
if pred.(Common.maybe_move_to_block(zipper)) do
index
else
case Zipper.right(zipper) do
nil ->
nil
zipper ->
zipper
|> find_index_right(pred, index + 1)
end
end
end
end