Current section

Files

Jump to
igniter lib igniter code list.ex
Raw

lib/igniter/code/list.ex

# SPDX-FileCopyrightText: 2024 igniter contributors <https://github.com/ash-project/igniter/graphs/contributors>
#
# SPDX-License-Identifier: MIT
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 already 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
Common.within(zipper, fn zipper ->
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)
end
@doc "Appends `quoted` to the list unless it is already 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 ->
append_to_list(zipper, quoted)
_ ->
{:ok, 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_single_child_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_single_child_block()
|> Zipper.append_child(quoted)}
else
:error
end
end
@spec remove_from_list(Zipper.t(), predicate :: (Zipper.t() -> boolean())) ::
{:ok, Zipper.t()} | :error
def remove_from_list(zipper, predicate) do
zipper = Igniter.Code.Common.maybe_move_to_single_child_block(zipper)
if list?(zipper) do
Common.within(zipper, fn zipper ->
zipper
|> Zipper.down()
|> case do
nil ->
{:ok, zipper}
zipper ->
zipper
|> Common.move_right(predicate)
|> case do
:error ->
:error
{:ok, zipper} ->
{:ok, Zipper.remove(zipper)}
end
end
end)
|> case do
:error ->
{:ok, zipper}
{:ok, ^zipper} ->
{:ok, zipper}
{:ok, zipper} ->
remove_from_list(zipper, predicate)
end
else
:error
end
end
@spec replace_in_list(Zipper.t(), predicate :: (Zipper.t() -> boolean()), term :: any()) ::
{:ok, Zipper.t()} | :error
def replace_in_list(zipper, predicate, value) do
if list?(zipper) do
Common.within(zipper, fn zipper ->
zipper
|> Zipper.down()
|> Common.move_right(predicate)
|> case do
:error ->
:error
{:ok, zipper} ->
{:ok, Igniter.Code.Common.replace_code(zipper, value)}
end
end)
|> case do
:error ->
{:ok, zipper}
{:ok, zipper} ->
replace_in_list(zipper, predicate, value)
end
else
:error
end
end
@doc "Removes the item at the given index, returning `:error` if nothing is at that index"
@spec remove_index(Zipper.t(), index :: non_neg_integer()) :: {:ok, Zipper.t()} | :error
def remove_index(zipper, index) do
if list?(zipper) do
Common.within(zipper, fn zipper ->
zipper
|> Igniter.Code.Common.maybe_move_to_single_child_block()
|> Zipper.down()
|> Common.move_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(), (Zipper.t() -> boolean())) :: integer() | nil
def find_list_item_index(zipper, pred) do
# go into first list item
zipper
|> Common.maybe_move_to_single_child_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(), (Zipper.t() -> boolean())) :: {:ok, Zipper.t()} | :error
def move_to_list_item(zipper, pred) do
# go into first list item
zipper
|> Common.maybe_move_to_single_child_block()
|> Zipper.down()
|> case do
nil ->
:error
zipper ->
do_move_to_list_item(zipper, pred)
end
end
@doc "Applies the given function to every element of the list. The passed function must return `{:ok, zipper}` or `:error` When `map` returns, the Zipper will point to the final element of the list."
@spec map(Zipper.t(), (Zipper.t() -> {:ok, Zipper.t()})) :: {:ok, Zipper.t()} | :error
def map(zipper, fun) do
# go into first list item
zipper
|> Common.maybe_move_to_single_child_block()
|> Zipper.down()
|> case do
nil ->
:error
zipper ->
do_map(zipper, fun)
end
end
defp do_map(zipper, fun) do
case Igniter.Code.Common.within(zipper, fun) do
{:ok, zipper} ->
case Zipper.right(zipper) do
nil ->
{:ok, zipper}
right ->
do_map(right, fun)
end
:error ->
:error
end
end
@doc "Moves to the list item matching the given predicate, assuming you are currently inside the list"
def do_move_to_list_item(zipper, pred) do
if pred.(zipper) do
{:ok, zipper}
else
zipper
|> Zipper.right()
|> case do
nil ->
:error
right ->
do_move_to_list_item(right, pred)
end
end
end
defp find_index_right(zipper, pred, index) do
if pred.(Common.maybe_move_to_single_child_block(zipper)) do
index
else
case Zipper.right(zipper) do
nil ->
nil
zipper ->
zipper
|> find_index_right(pred, index + 1)
end
end
end
end