Files
n8n-openai-adapter/lib/elixir/lib/code/fragment.ex
T

1347 lines
48 KiB
Elixir

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
defmodule Code.Fragment do
@moduledoc """
This module provides conveniences for analyzing fragments of
textual code and extract available information whenever possible.
This module should be considered experimental.
"""
@type position :: {line :: pos_integer(), column :: pos_integer()}
@typedoc """
Options for cursor context functions.
Currently, these options are not used but reserved for future extensibility.
"""
@type cursor_opts :: []
@typedoc """
Options for converting code fragments to quoted expressions.
"""
@type container_cursor_to_quoted_opts :: [
file: String.t(),
line: pos_integer(),
column: pos_integer(),
columns: boolean(),
token_metadata: boolean(),
literal_encoder: (term(), Macro.metadata() -> term()),
trailing_fragment: String.t()
]
@doc ~S"""
Returns the list of lines in the given string, preserving their line endings.
Only the line endings recognized by the Elixir compiler are
considered, namely `\r\n` and `\n`. If you would like the retrieve
lines without their line endings, use `String.split(string, ["\r\n", "\n"])`.
## Examples
iex> Code.Fragment.lines("foo\r\nbar\r\nbaz")
["foo\r\n", "bar\r\n", "baz"]
iex> Code.Fragment.lines("foo\nbar\nbaz")
["foo\n", "bar\n", "baz"]
iex> Code.Fragment.lines("")
[""]
"""
@doc since: "1.19.0"
def lines(string) do
lines(string, <<>>)
end
defp lines(<<?\n, rest::binary>>, acc),
do: [<<acc::binary, ?\n>> | lines(rest, <<>>)]
defp lines(<<char, rest::binary>>, acc),
do: lines(rest, <<acc::binary, char>>)
defp lines(<<>>, acc),
do: [acc]
@doc """
Receives a string and returns the cursor context.
This function receives a string with an Elixir code fragment,
representing a cursor position, and based on the string, it
provides contextual information about the latest token.
The return of this function can then be used to provide tips,
suggestions, and autocompletion functionality.
This function performs its analyses on tokens. This means
it does not understand how constructs are nested within each
other. See the "Limitations" section below.
Consider adding a catch-all clause when handling the return
type of this function as new cursor information may be added
in future releases.
## Examples
iex> Code.Fragment.cursor_context("")
:expr
iex> Code.Fragment.cursor_context("hello_wor")
{:local_or_var, ~c"hello_wor"}
## Return values
* `{:alias, charlist}` - the context is an alias, potentially
a nested one, such as `Hello.Wor` or `HelloWor`
* `{:alias, inside_alias, charlist}` - the context is an alias, potentially
a nested one, where `inside_alias` is an expression `{:module_attribute, charlist}`
or `{:local_or_var, charlist}` and `charlist` is a static part
Examples are `__MODULE__.Submodule` or `@hello.Submodule`
* `{:block_keyword_or_binary_operator, charlist}` - may be a block keyword (do, end, after,
catch, else, rescue) or a binary operator
* `{:dot, inside_dot, charlist}` - the context is a dot
where `inside_dot` is either a `{:var, charlist}`, `{:alias, charlist}`,
`{:module_attribute, charlist}`, `{:unquoted_atom, charlist}` or a `dot`
itself. If a var is given, this may either be a remote call or a map
field access. Examples are `Hello.wor`, `:hello.wor`, `hello.wor`,
`Hello.nested.wor`, `hello.nested.wor`, and `@hello.world`. If `charlist`
is empty and `inside_dot` is an alias, then the autocompletion may either
be an alias or a remote call.
* `{:dot_arity, inside_dot, charlist}` - the context is a dot arity
where `inside_dot` is either a `{:var, charlist}`, `{:alias, charlist}`,
`{:module_attribute, charlist}`, `{:unquoted_atom, charlist}` or a `dot`
itself. If a var is given, it must be a remote arity. Examples are
`Hello.world/`, `:hello.world/`, `hello.world/2`, and `@hello.world/2`
* `{:dot_call, inside_dot, charlist}` - the context is a dot
call. This means parentheses or space have been added after the expression.
where `inside_dot` is either a `{:var, charlist}`, `{:alias, charlist}`,
`{:module_attribute, charlist}`, `{:unquoted_atom, charlist}` or a `dot`
itself. If a var is given, it must be a remote call. Examples are
`Hello.world(`, `:hello.world(`, `Hello.world `, `hello.world(`, `hello.world `,
and `@hello.world(`
* `:expr` - may be any expression. Autocompletion may suggest an alias,
local or var
* `{:local_or_var, charlist}` - the context is a variable or a local
(import or local) call, such as `hello_wor`
* `{:local_arity, charlist}` - the context is a local (import or local)
arity, such as `hello_world/`
* `{:local_call, charlist}` - the context is a local (import or local)
call, such as `hello_world(` and `hello_world `
* `{:anonymous_call, inside_caller}` - the context is an anonymous
call, such as `fun.(` and `@fun.(`.
* `{:module_attribute, charlist}` - the context is a module attribute,
such as `@hello_wor`
* `{:operator, charlist}` - the context is an operator, such as `+` or
`==`. Note textual operators, such as `when` do not appear as operators
but rather as `:local_or_var`. `@` is never an `:operator` and always a
`:module_attribute`
* `{:operator_arity, charlist}` - the context is an operator arity, which
is an operator followed by /, such as `+/`, `not/` or `when/`
* `{:operator_call, charlist}` - the context is an operator call, which is
an operator followed by space, such as `left + `, `not ` or `x when `
* `:none` - no context possible
* `{:sigil, charlist}` - the context is a sigil. It may be either the beginning
of a sigil, such as `~` or `~s`, or an operator starting with `~`, such as
`~>` and `~>>`
* `{:struct, inside_struct}` - the context is a struct, such as `%`, `%UR` or `%URI`.
`inside_struct` can either be a `charlist` in case of a static alias or an
expression `{:alias, inside_alias, charlist}`, `{:module_attribute, charlist}`,
`{:local_or_var, charlist}`, `{:dot, inside_dot, charlist}`
* `{:unquoted_atom, charlist}` - the context is an unquoted atom. This
can be any atom or an atom representing a module
We recommend looking at the test suite of this function for a complete list
of examples and their return values.
## Limitations
The analysis is based on the current token, by analysing the last line of
the input. For example, this code:
iex> Code.Fragment.cursor_context("%URI{")
:expr
returns `:expr`, which suggests any variable, local function or alias
could be used. However, given we are inside a struct, the best suggestion
would be a struct field. In such cases, you can use
`container_cursor_to_quoted`, which will return the container of the AST
the cursor is currently within. You can then analyse this AST to provide
completion of field names.
As a consequence of its token-based implementation, this function considers
only the last line of the input. This means it will show suggestions inside
strings, heredocs, etc, which is intentional as it helps with doctests,
references, and more.
"""
@doc since: "1.13.0"
@spec cursor_context(List.Chars.t(), cursor_opts()) ::
{:alias, charlist}
| {:alias, inside_alias, charlist}
| {:block_keyword_or_binary_operator, charlist}
| {:dot, inside_dot, charlist}
| {:dot_arity, inside_dot, charlist}
| {:dot_call, inside_dot, charlist}
| :expr
| {:local_or_var, charlist}
| {:local_arity, charlist}
| {:local_call, charlist}
| {:anonymous_call, inside_caller}
| {:module_attribute, charlist}
| {:operator, charlist}
| {:operator_arity, charlist}
| {:operator_call, charlist}
| :none
| {:sigil, charlist}
| {:struct, inside_struct}
| {:unquoted_atom, charlist}
when inside_dot:
{:alias, charlist}
| {:alias, inside_alias, charlist}
| {:dot, inside_dot, charlist}
| {:module_attribute, charlist}
| {:unquoted_atom, charlist}
| {:var, charlist}
| :expr,
inside_alias:
{:local_or_var, charlist}
| {:module_attribute, charlist},
inside_struct:
charlist
| {:alias, inside_alias, charlist}
| {:local_or_var, charlist}
| {:module_attribute, charlist}
| {:dot, inside_dot, charlist},
inside_caller: {:var, charlist} | {:module_attribute, charlist}
def cursor_context(fragment, opts \\ [])
def cursor_context(fragment, opts)
when (is_binary(fragment) or is_list(fragment)) and is_list(opts) do
fragment
|> last_line()
|> :lists.reverse()
|> codepoint_cursor_context(opts)
|> elem(0)
end
def cursor_context(other, opts) when is_list(opts) do
cursor_context(to_charlist(other), opts)
end
@operators ~c"\\<>+-*/:=|&~^%!$"
@starting_punctuation ~c",([{;"
@closing_punctuation ~c")]}\"'"
@space ~c"\t\s"
@trailing_identifier ~c"?!"
@tilde_op_prefix ~c"<=~"
@non_identifier @trailing_identifier ++
@operators ++ @starting_punctuation ++ @closing_punctuation ++ @space ++ [?.]
@textual_operators ~w(when not and or in)c
@keywords ~w(do end after else catch rescue fn true false nil)c
defp codepoint_cursor_context(reverse, _opts) do
{stripped, spaces} = strip_spaces(reverse, 0)
case stripped do
# It is empty
[] -> {:expr, 0}
# Structs
[?%, ?:, ?: | _] -> {{:struct, ~c""}, 1}
[?%, ?: | _] -> {{:unquoted_atom, ~c"%"}, 2}
[?% | _] -> {{:struct, ~c""}, 1}
# Token/AST only operators
[?>, ?= | rest] when rest == [] or hd(rest) != ?: -> {:expr, 0}
[?>, ?- | rest] when rest == [] or hd(rest) != ?: -> {:expr, 0}
# Two-digit containers
[?<, ?< | rest] when rest == [] or hd(rest) != ?< -> {:expr, 0}
# Ambiguity around :
[?: | rest] when rest == [] or hd(rest) != ?: -> unquoted_atom_or_expr(spaces)
# Dots
[?.] -> {:none, 0}
[?. | rest] when hd(rest) not in ~c".:" -> dot(rest, spaces + 1, ~c"")
# It is a local or remote call with parens
[?( | rest] -> call_to_cursor_context(strip_spaces(rest, spaces + 1))
# A local arity definition
[?/ | rest] -> arity_to_cursor_context(strip_spaces(rest, spaces + 1))
# Starting a new expression
[h | _] when h in @starting_punctuation -> {:expr, 0}
# It is keyword, binary operator, a local or remote call without parens
rest when spaces > 0 -> closing_or_call_to_cursor_context({rest, spaces})
# It is an identifier
_ -> identifier_to_cursor_context(reverse, spaces, false)
end
end
defp strip_spaces([h | rest], count) when h in @space, do: strip_spaces(rest, count + 1)
defp strip_spaces(rest, count), do: {rest, count}
defp unquoted_atom_or_expr(0), do: {{:unquoted_atom, ~c""}, 1}
defp unquoted_atom_or_expr(_), do: {:expr, 0}
defp arity_to_cursor_context({reverse, spaces}) do
case identifier_to_cursor_context(reverse, spaces, true) do
{{:local_or_var, acc}, count} -> {{:local_arity, acc}, count}
{{:dot, base, acc}, count} -> {{:dot_arity, base, acc}, count}
{{:operator, acc}, count} -> {{:operator_arity, acc}, count}
{{:sigil, _}, _} -> {:none, 0}
{_, _} -> {{:operator, ~c"/"}, 1}
end
end
defp call_to_cursor_context({reverse, spaces}) do
with [?. | rest] <- reverse,
{rest, spaces} = strip_spaces(rest, spaces),
[h | _] when h not in @non_identifier <- rest do
case identifier_to_cursor_context(rest, spaces, true) do
{{:local_or_var, acc}, count} -> {{:anonymous_call, {:var, acc}}, count + 1}
{{:module_attribute, _} = attr, count} -> {{:anonymous_call, attr}, count + 1}
{_, _} -> {:none, 0}
end
else
_ ->
case identifier_to_cursor_context(reverse, spaces, true) do
{{:local_or_var, acc}, count} -> {{:local_call, acc}, count}
{{:dot, base, acc}, count} -> {{:dot_call, base, acc}, count}
{{:operator, acc}, count} -> {{:operator_call, acc}, count}
{_, _} -> {:none, 0}
end
end
end
defp closing_or_call_to_cursor_context({reverse, spaces}) do
if closing?(reverse) do
{{:block_keyword_or_binary_operator, ~c""}, 0}
else
call_to_cursor_context({reverse, spaces})
end
end
defp identifier_to_cursor_context([?., ?., ?: | _], n, _), do: {{:unquoted_atom, ~c".."}, n + 3}
defp identifier_to_cursor_context([?., ?., ?. | _], n, _), do: {{:operator, ~c"..."}, n + 3}
defp identifier_to_cursor_context([?., ?: | _], n, _), do: {{:unquoted_atom, ~c"."}, n + 2}
defp identifier_to_cursor_context([?., ?. | _], n, _), do: {{:operator, ~c".."}, n + 2}
defp identifier_to_cursor_context(reverse, count, call_op?) do
case identifier(reverse, count) do
:none ->
{:none, 0}
:operator ->
operator(reverse, count, [], call_op?)
{:struct, {:module_attribute, acc}, count} ->
{{:struct, {:module_attribute, acc}}, count + 1}
{:module_attribute, acc, count} ->
{{:module_attribute, acc}, count}
{:sigil, acc, count} ->
{{:sigil, acc}, count}
{:unquoted_atom, acc, count} ->
{{:unquoted_atom, acc}, count}
{:alias, rest, acc, count} ->
case strip_spaces(rest, count) do
{~c"." ++ rest, count} when rest == [] or hd(rest) != ?. ->
nested_alias(rest, count + 1, acc)
{~c"%" ++ _, count} ->
{{:struct, acc}, count + 1}
_ ->
{{:alias, acc}, count}
end
{:identifier, _, acc, count} when call_op? and acc in @textual_operators ->
{{:operator, acc}, count}
{:identifier, [?%], acc, count} ->
case identifier_to_cursor_context(acc |> Enum.reverse(), count, true) do
{{:local_or_var, _} = identifier, _} -> {{:struct, identifier}, count + 1}
_ -> {:none, 0}
end
{:identifier, rest, acc, count} ->
case strip_spaces(rest, count) do
{~c"." ++ rest, count} when rest == [] or hd(rest) != ?. ->
dot(rest, count + 1, acc)
{rest, rest_count} ->
response =
if rest_count > count and closing?(rest),
do: :block_keyword_or_binary_operator,
else: :local_or_var
{{response, acc}, count}
end
{:capture_arg, acc, count} ->
{{:capture_arg, acc}, count}
end
end
# If it is a closing punctuation
defp closing?([h | _]) when h in @closing_punctuation, do: true
# Closing bitstring (but deal with operators)
defp closing?([?>, ?> | rest]), do: rest == [] or hd(rest) not in [?>, ?~]
# Keywords
defp closing?(rest) do
case split_non_identifier(rest, []) do
{~c"nil", _} -> true
{~c"true", _} -> true
{~c"false", _} -> true
{[digit | _], _} when digit in ?0..?9 -> true
{[upper | _], _} when upper in ?A..?Z -> true
{[_ | _], [?: | rest]} -> rest == [] or hd(rest) != ?:
{_, _} -> false
end
end
defp split_non_identifier([h | t], acc) when h not in @non_identifier,
do: split_non_identifier(t, [h | acc])
defp split_non_identifier(rest, acc), do: {acc, rest}
defp identifier([?? | rest], count), do: check_identifier(rest, count + 1, [??])
defp identifier([?! | rest], count), do: check_identifier(rest, count + 1, [?!])
defp identifier(rest, count), do: check_identifier(rest, count, [])
defp check_identifier([h | t], count, acc) when h not in @non_identifier,
do: rest_identifier(t, count + 1, [h | acc])
defp check_identifier(_, _, _), do: :operator
defp rest_identifier([h | rest], count, acc) when h not in @non_identifier do
rest_identifier(rest, count + 1, [h | acc])
end
defp rest_identifier(rest, count, [?@ | acc]) do
case tokenize_identifier(rest, count, acc) do
{:identifier, [?% | _rest], acc, count} -> {:struct, {:module_attribute, acc}, count}
{:identifier, _rest, acc, count} -> {:module_attribute, acc, count}
:none when acc == [] -> {:module_attribute, ~c"", count}
_ -> :none
end
end
defp rest_identifier([?~ | rest], count, [letter])
when (letter in ?A..?Z or letter in ?a..?z) and
(rest == [] or hd(rest) not in @tilde_op_prefix) do
{:sigil, [letter], count + 1}
end
defp rest_identifier([?: | rest], count, acc) when rest == [] or hd(rest) != ?: do
case String.Tokenizer.tokenize(acc) do
{_, _, [], _, _, _} -> {:unquoted_atom, acc, count + 1}
_ -> :none
end
end
defp rest_identifier([?? | _], _count, _acc) do
:none
end
defp rest_identifier([?& | tail] = rest, count, acc) when tail == [] or hd(tail) != ?& do
if Enum.all?(acc, &(&1 in ?0..?9)) do
{:capture_arg, [?& | acc], count + 1}
else
tokenize_identifier(rest, count, acc)
end
end
defp rest_identifier(rest, count, acc) do
tokenize_identifier(rest, count, acc)
end
defp tokenize_identifier(rest, count, acc) do
case String.Tokenizer.tokenize(acc) do
# Not actually an atom cause rest is not a :
{:atom, _, _, _, _, _} ->
:none
# Aliases must be ascii only
{:alias, _, _, _, false, _} ->
:none
{kind, _, [], _, _, extra} ->
if :at in extra do
:none
else
{kind, rest, acc, count}
end
_ ->
:none
end
end
defp nested_alias(rest, count, acc) do
{rest, count} = strip_spaces(rest, count)
case identifier_to_cursor_context(rest, count, true) do
{{:struct, prev}, count} when is_list(prev) ->
{{:struct, prev ++ ~c"." ++ acc}, count}
{{:struct, {:alias, parent, prev}}, count} ->
{{:struct, {:alias, parent, prev ++ ~c"." ++ acc}}, count}
{{:struct, prev}, count} ->
{{:struct, {:alias, prev, acc}}, count}
{{:alias, prev}, count} ->
{{:alias, prev ++ ~c"." ++ acc}, count}
{{:alias, parent, prev}, count} ->
{{:alias, parent, prev ++ ~c"." ++ acc}, count}
{{:local_or_var, prev}, count} ->
{{:alias, {:local_or_var, prev}, acc}, count}
{{:module_attribute, prev}, count} ->
{{:alias, {:module_attribute, prev}, acc}, count}
_ ->
{:none, 0}
end
end
defp dot(rest, count, acc) do
{rest, count} = strip_spaces(rest, count)
case identifier_to_cursor_context(rest, count, true) do
{{:local_or_var, var}, count} ->
{{:dot, {:var, var}, acc}, count}
{{:unquoted_atom, _} = prev, count} ->
{{:dot, prev, acc}, count}
{{:alias, _} = prev, count} ->
{{:dot, prev, acc}, count}
{{:alias, _, _} = prev, count} ->
{{:dot, prev, acc}, count}
{{:struct, inner}, count} when is_list(inner) ->
{{:struct, {:dot, {:alias, inner}, acc}}, count}
{{:struct, inner}, count} ->
{{:struct, {:dot, inner, acc}}, count}
{{:dot, _, _} = prev, count} ->
{{:dot, prev, acc}, count}
{{:module_attribute, _} = prev, count} ->
{{:dot, prev, acc}, count}
{:expr, count} ->
{{:dot, :expr, acc}, count}
{_, _} ->
{:none, 0}
end
end
defp operator([h | rest], count, acc, call_op?) when h in @operators do
operator(rest, count + 1, [h | acc], call_op?)
end
# If we are opening a sigil, ignore the operator.
defp operator([letter, ?~ | rest], _count, [op], _call_op?)
when op in ~c"<|/" and (letter in ?A..?Z or letter in ?a..?z) and
(rest == [] or hd(rest) not in @tilde_op_prefix) do
{:none, 0}
end
defp operator(rest, count, ~c"~", call_op?) do
{rest, _} = strip_spaces(rest, count)
if call_op? or match?([?. | rest] when rest == [] or hd(rest) != ?., rest) do
{:none, 0}
else
{{:sigil, ~c""}, count}
end
end
defp operator([?) | rest], _, [], true) when hd(rest) != ?? do
{:expr, 0}
end
defp operator(rest, count, acc, _call_op?) do
case :elixir_tokenizer.tokenize(acc, 1, 1, []) do
{:ok, _, _, _, [{:atom, _, _}], []} ->
{{:unquoted_atom, tl(acc)}, count}
{:ok, _, _, _, [{_, _, op}], []} ->
{rest, dot_count} = strip_spaces(rest, count)
cond do
Code.Identifier.unary_op(op) == :error and Code.Identifier.binary_op(op) == :error ->
{:none, 0}
match?([?. | rest] when rest == [] or hd(rest) != ?., rest) ->
dot(tl(rest), dot_count + 1, acc)
true ->
{{:operator, acc}, count}
end
_ ->
{:none, 0}
end
end
@doc """
Receives a string and returns the surround context.
This function receives a string with an Elixir code fragment
and a `position`. It returns a map containing the beginning
and ending of the identifier alongside its context, or `:none`
if there is nothing with a known context. This is useful to
provide mouse-over and highlight functionality in editors.
The difference between `cursor_context/2` and `surround_context/3`
is that the former assumes the expression in the code fragment
is incomplete. For example, `do` in `cursor_context/2` may be
a keyword or a variable or a local call, while `surround_context/3`
assumes the expression in the code fragment is complete, therefore
`do` would always be a keyword.
The `position` contains both the `line` and `column`, both starting
with the index of 1. The column must precede the surrounding expression.
For example, the expression `foo`, will return something for the columns
1, 2, and 3, but not 4:
foo
^ column 1
foo
^ column 2
foo
^ column 3
foo
^ column 4
The returned map contains the column the expression starts and the
first column after the expression ends.
Similar to `cursor_context/2`, this function is also token-based
and may not be accurate under all circumstances. See the
"Return values" and "Limitations" section under `cursor_context/2`
for more information.
## Examples
iex> Code.Fragment.surround_context("foo", {1, 1})
%{begin: {1, 1}, context: {:local_or_var, ~c"foo"}, end: {1, 4}}
## Differences to `cursor_context/2`
Because `surround_context/3` attempts to capture complex expressions,
it has some differences to `cursor_context/2`:
* `dot_call`/`dot_arity` and `operator_call`/`operator_arity`
are collapsed into `dot` and `operator` contexts respectively
as there aren't any meaningful distinctions between them
* On the other hand, this function still makes a distinction between
`local_call`/`local_arity` and `local_or_var`, since the latter can
be a local or variable
* `@` when not followed by any identifier is returned as `{:operator, ~c"@"}`
(in contrast to `{:module_attribute, ~c""}` in `cursor_context/2`
* This function never returns empty sigils `{:sigil, ~c""}` or empty structs
`{:struct, ~c""}` as context
* This function returns keywords as `{:keyword, ~c"do"}`
* This function never returns `:expr`
We recommend looking at the test suite of this function for a complete list
of examples and their return values.
"""
@doc since: "1.13.0"
@spec surround_context(List.Chars.t(), position(), cursor_opts()) ::
%{begin: position, end: position, context: context} | :none
when context:
{:alias, charlist}
| {:alias, inside_alias, charlist}
| {:dot, inside_dot, charlist}
| {:local_or_var, charlist}
| {:local_arity, charlist}
| {:local_call, charlist}
| {:module_attribute, charlist}
| {:operator, charlist}
| {:sigil, charlist}
| {:struct, inside_struct}
| {:unquoted_atom, charlist}
| {:keyword, charlist}
| {:key, charlist}
| {:capture_arg, charlist},
inside_dot:
{:alias, charlist}
| {:alias, inside_alias, charlist}
| {:dot, inside_dot, charlist}
| {:module_attribute, charlist}
| {:unquoted_atom, charlist}
| {:var, charlist}
| :expr,
inside_alias:
{:local_or_var, charlist}
| {:module_attribute, charlist},
inside_struct:
charlist
| {:alias, inside_alias, charlist}
| {:local_or_var, charlist}
| {:module_attribute, charlist}
| {:dot, inside_dot, charlist}
def surround_context(fragment, position, options \\ [])
def surround_context(string, {line, column}, opts)
when (is_binary(string) or is_list(string)) and is_list(opts) do
{charlist, lines_before_lengths, lines_current_and_after_lengths} =
surround_line(string, line, column)
prepended_columns = Enum.sum(lines_before_lengths)
charlist
|> position_surround_context(line, column + prepended_columns, opts)
|> to_multiline_range(
prepended_columns,
lines_before_lengths,
lines_current_and_after_lengths
)
end
def surround_context(other, {_, _} = position, opts) do
surround_context(to_charlist(other), position, opts)
end
defp position_surround_context(charlist, line, column, opts)
when is_integer(line) and line >= 1 and is_integer(column) and column >= 1 do
{reversed_pre, post} = string_reverse_at(charlist, column - 1, [])
{reversed_pre, post} = adjust_position(reversed_pre, post)
case take_identifier(post, []) do
{_, [], _} ->
maybe_operator(reversed_pre, post, line, opts)
{:identifier, reversed_post, rest} ->
{keyword_key?, rest} =
case rest do
[?: | tail] when tail == [] or hd(tail) in @space ->
{true, rest}
_ ->
{rest, _} = strip_spaces(rest, 0)
{false, rest}
end
reversed = reversed_post ++ reversed_pre
case codepoint_cursor_context(reversed, opts) do
{{:struct, acc}, offset} ->
build_surround({:struct, acc}, reversed, line, offset)
{{:alias, acc}, offset} ->
build_surround({:alias, acc}, reversed, line, offset)
{{:alias, parent, acc}, offset} ->
build_surround({:alias, parent, acc}, reversed, line, offset)
{{:dot, _, [_ | _]} = dot, offset} ->
build_surround(dot, reversed, line, offset)
{{:local_or_var, acc}, offset} when keyword_key? ->
build_surround({:key, acc}, reversed, line, offset)
{{:local_or_var, acc}, offset} when hd(rest) == ?( ->
build_surround({:local_call, acc}, reversed, line, offset)
{{:local_or_var, acc}, offset} when hd(rest) == ?/ ->
build_surround({:local_arity, acc}, reversed, line, offset)
{{:local_or_var, acc}, offset} when acc in @textual_operators ->
build_surround({:operator, acc}, reversed, line, offset)
{{:local_or_var, acc}, offset} when acc in @keywords ->
build_surround({:keyword, acc}, reversed, line, offset)
{{:local_or_var, acc}, offset} ->
build_surround({:local_or_var, acc}, reversed, line, offset)
{{:block_keyword_or_binary_operator, acc}, offset} when acc in @textual_operators ->
build_surround({:operator, acc}, reversed, line, offset)
{{:block_keyword_or_binary_operator, acc}, offset} when acc in @keywords ->
build_surround({:keyword, acc}, reversed, line, offset)
{{:module_attribute, ~c""}, offset} ->
build_surround({:operator, ~c"@"}, reversed, line, offset)
{{:module_attribute, acc}, offset} ->
build_surround({:module_attribute, acc}, reversed, line, offset)
{{:sigil, acc}, offset} ->
build_surround({:sigil, acc}, reversed, line, offset)
{{:unquoted_atom, acc}, offset} ->
build_surround({:unquoted_atom, acc}, reversed, line, offset)
{{:capture_arg, acc}, offset} ->
build_surround({:capture_arg, acc}, reversed, line, offset)
_ ->
maybe_operator(reversed_pre, post, line, opts)
end
{:alias, reversed_post, _rest} ->
reversed = reversed_post ++ reversed_pre
case codepoint_cursor_context(reversed, opts) do
{{:alias, acc}, offset} ->
build_surround({:alias, acc}, reversed, line, offset)
{{:alias, parent, acc}, offset} ->
build_surround({:alias, parent, acc}, reversed, line, offset)
{{:struct, acc}, offset} ->
build_surround({:struct, acc}, reversed, line, offset)
_ ->
:none
end
end
end
defp maybe_operator(reversed_pre, post, line, opts) do
case take_operator(post, []) do
{[], _rest} ->
:none
{reversed_post, rest} ->
reversed = reversed_post ++ reversed_pre
case codepoint_cursor_context(reversed, opts) do
{{:operator, ~c"&"}, offset} when hd(rest) in ?0..?9 ->
arg = Enum.take_while(rest, &(&1 in ?0..?9))
build_surround(
{:capture_arg, ~c"&" ++ arg},
:lists.reverse(arg, reversed),
line,
offset + length(arg)
)
{{:operator, acc}, offset} ->
build_surround({:operator, acc}, reversed, line, offset)
{{:sigil, ~c""}, offset} when hd(rest) in ?A..?Z or hd(rest) in ?a..?z ->
build_surround({:sigil, [hd(rest)]}, [hd(rest) | reversed], line, offset + 1)
{{:dot, _, [_ | _]} = dot, offset} ->
build_surround(dot, reversed, line, offset)
_ ->
:none
end
end
end
defp build_surround(context, reversed, line, offset) do
{post, reversed_pre} = enum_reverse_at(reversed, offset, [])
pre = :lists.reverse(reversed_pre)
pre_length = :string.length(pre) + 1
%{
context: context,
begin: {line, pre_length},
end: {line, pre_length + :string.length(post)}
}
end
defp take_identifier([h | t], acc) when h in @trailing_identifier,
do: {:identifier, [h | acc], t}
defp take_identifier([h | t], acc) when h not in @non_identifier,
do: take_identifier(t, [h | acc])
defp take_identifier(rest, acc) do
with {[?. | t], _} <- strip_spaces(rest, 0),
{[h | _], _} when h in ?A..?Z <- strip_spaces(t, 0) do
take_alias(rest, acc)
else
_ -> {:identifier, acc, rest}
end
end
defp take_alias([h | t], acc) when h in ?A..?Z or h in ?a..?z or h in ?0..?9 or h == ?_,
do: take_alias(t, [h | acc])
defp take_alias(rest, acc) do
with {[?. | t], acc} <- move_spaces(rest, acc),
{[h | t], acc} when h in ?A..?Z <- move_spaces(t, [?. | acc]) do
take_alias(t, [h | acc])
else
_ -> {:alias, acc, rest}
end
end
defp take_operator([h | t], acc) when h in @operators, do: take_operator(t, [h | acc])
defp take_operator([h | t], acc) when h == ?., do: take_operator(t, [h | acc])
defp take_operator(rest, acc), do: {acc, rest}
# Unquoted atom handling
defp adjust_position(reversed_pre, [?: | post])
when hd(post) != ?: and (reversed_pre == [] or hd(reversed_pre) != ?:) do
{[?: | reversed_pre], post}
end
defp adjust_position(reversed_pre, [?% | post]) do
adjust_position([?% | reversed_pre], post)
end
# Dot/struct handling
defp adjust_position(reversed_pre, post) do
case move_spaces(post, reversed_pre) do
# If we are between spaces and a dot, move past the dot
{[?. | post], reversed_pre} when hd(post) != ?. and hd(reversed_pre) != ?. ->
{post, reversed_pre} = move_spaces(post, [?. | reversed_pre])
{reversed_pre, post}
_ ->
case strip_spaces(reversed_pre, 0) do
# If there is a dot to our left, make sure to move to the first character
{[?. | rest], _} when rest == [] or hd(rest) not in ~c".:" ->
{post, reversed_pre} = move_spaces(post, reversed_pre)
{reversed_pre, post}
# If there is a % to our left, make sure to move to the first character
{[?% | _], _} ->
case move_spaces(post, reversed_pre) do
{[h | _] = post, reversed_pre} when h in ?A..?Z ->
{reversed_pre, post}
_ ->
{reversed_pre, post}
end
_ ->
{reversed_pre, post}
end
end
end
defp move_spaces([h | t], acc) when h in @space, do: move_spaces(t, [h | acc])
defp move_spaces(t, acc), do: {t, acc}
defp string_reverse_at(charlist, 0, acc), do: {acc, charlist}
defp string_reverse_at(charlist, n, acc) do
case :unicode_util.gc(charlist) do
[gc | cont] when is_integer(gc) -> string_reverse_at(cont, n - 1, [gc | acc])
[gc | cont] when is_list(gc) -> string_reverse_at(cont, n - 1, :lists.reverse(gc, acc))
[] -> {acc, []}
end
end
defp enum_reverse_at([h | t], n, acc) when n > 0, do: enum_reverse_at(t, n - 1, [h | acc])
defp enum_reverse_at(rest, _, acc), do: {acc, rest}
defp last_line(binary) when is_binary(binary) do
[last_line | lines_reverse] =
binary
|> String.split(["\r\n", "\n"])
|> Enum.reverse()
prepend_cursor_lines(lines_reverse, String.to_charlist(last_line))
end
defp last_line(charlist) when is_list(charlist) do
[last_line | lines_reverse] =
charlist
|> :string.replace(~c"\r\n", ~c"\n", :all)
|> :string.join(~c"")
|> :string.split(~c"\n", :all)
|> Enum.reverse()
prepend_cursor_lines(lines_reverse, last_line)
end
defp prepend_cursor_lines(lines, last_line) do
with [line | lines] <- lines,
{trimmed_line, incomplete?} = ends_as_incomplete(to_charlist(line), [], true),
true <- incomplete? or starts_with_dot?(last_line) do
prepend_cursor_lines(lines, Enum.reverse(trimmed_line, last_line))
else
_ -> last_line
end
end
defp starts_with_dot?([?. | _]), do: true
defp starts_with_dot?([h | t]) when h in @space, do: starts_with_dot?(t)
defp starts_with_dot?(_), do: false
defp ends_as_incomplete([?# | _], acc, incomplete?),
do: {acc, incomplete?}
defp ends_as_incomplete([h | t], acc, _incomplete?) when h in [?(, ?.],
do: ends_as_incomplete(t, [h | acc], true)
defp ends_as_incomplete([h | t], acc, incomplete?) when h in @space,
do: ends_as_incomplete(t, [h | acc], incomplete?)
defp ends_as_incomplete([h | t], acc, _incomplete?),
do: ends_as_incomplete(t, [h | acc], false)
defp ends_as_incomplete([], acc, incomplete?),
do: {acc, incomplete?}
defp surround_line(binary, line, column) when is_binary(binary) do
binary
|> String.split(["\r\n", "\n"])
|> Enum.map(&String.to_charlist/1)
|> surround_lines(line, column)
end
defp surround_line(charlist, line, column) when is_list(charlist) do
charlist
|> :string.replace(~c"\r\n", ~c"\n", :all)
|> :string.join(~c"")
|> :string.split(~c"\n", :all)
|> surround_lines(line, column)
end
defp surround_lines(lines, line, column) do
{lines_before_reverse, cursor_line, lines_after} = split_at(lines, line, [])
{trimmed_cursor_line, incomplete?} = ends_as_incomplete(to_charlist(cursor_line), [], true)
reversed_cursor_line =
if column - 1 > length(trimmed_cursor_line) do
# Don't strip comments if cursor is inside a comment
Enum.reverse(cursor_line)
else
trimmed_cursor_line
end
{cursor_line, after_lengths} =
append_surround_lines(lines_after, [], [reversed_cursor_line], incomplete?)
{cursor_line, before_lengths} = prepend_surround_lines(lines_before_reverse, [], cursor_line)
{cursor_line, before_lengths, [length(reversed_cursor_line) | after_lengths]}
end
defp split_at([line], _, acc), do: {acc, line, []}
defp split_at([line | lines], 1, acc), do: {acc, line, lines}
defp split_at([line | lines], count, acc), do: split_at(lines, count - 1, [line | acc])
defp prepend_surround_lines(lines, lengths, last_line) do
with [line | lines] <- lines,
{trimmed_line, incomplete?} = ends_as_incomplete(to_charlist(line), [], true),
true <- incomplete? or starts_with_dot?(last_line) do
lengths = [length(trimmed_line) | lengths]
prepend_surround_lines(lines, lengths, Enum.reverse(trimmed_line, last_line))
else
_ -> {last_line, Enum.reverse(lengths)}
end
end
defp append_surround_lines(lines, lengths, acc_lines, incomplete?) do
with [line | lines] <- lines,
line = to_charlist(line),
true <- incomplete? or starts_with_dot?(line) do
{trimmed_line, incomplete?} = ends_as_incomplete(line, [], true)
lengths = [length(trimmed_line) | lengths]
append_surround_lines(lines, lengths, [trimmed_line | acc_lines], incomplete?)
else
_ -> {Enum.reduce(acc_lines, [], &Enum.reverse/2), Enum.reverse(lengths)}
end
end
defp to_multiline_range(:none, _, _, _), do: :none
defp to_multiline_range(
%{begin: {begin_line, begin_column}, end: {end_line, end_column}} = context,
prepended,
lines_before_lengths,
lines_current_and_after_lengths
) do
{begin_line, begin_column} =
Enum.reduce_while(lines_before_lengths, {begin_line, begin_column - prepended}, fn
line_length, {acc_line, acc_column} ->
if acc_column < 1 do
{:cont, {acc_line - 1, acc_column + line_length}}
else
{:halt, {acc_line, acc_column}}
end
end)
{end_line, end_column} =
Enum.reduce_while(lines_current_and_after_lengths, {end_line, end_column - prepended}, fn
line_length, {acc_line, acc_column} ->
if acc_column > line_length + 1 do
{:cont, {acc_line + 1, acc_column - line_length}}
else
{:halt, {acc_line, acc_column}}
end
end)
%{context | begin: {begin_line, begin_column}, end: {end_line, end_column}}
end
@doc """
Receives a string and returns a quoted expression
with the cursor AST position within its parent expression.
This function receives a string with an Elixir code fragment,
representing a cursor position, and converts such string to
AST with the inclusion of special `__cursor__()` node representing
the cursor position within its container (i.e. its parent).
For example, take this code, which would be given as input:
max(some_value,
This function will return the AST equivalent to:
max(some_value, __cursor__())
In other words, this function is capable of closing any open
brackets and insert the cursor position. Other content at the
cursor position which is not a parent is discarded.
For example, if this is given as input:
max(some_value, another_val
It will return the same AST:
max(some_value, __cursor__())
Similarly, if only this is given:
max(some_va
Then it returns:
max(__cursor__())
Calls without parenthesis are also supported, as we assume the
brackets are implicit.
Tuples, lists, maps, and binaries all retain the cursor position:
max(some_value, [1, 2,
Returns the following AST:
max(some_value, [1, 2, __cursor__()])
Keyword lists (and do-end blocks) are also retained. The following:
if(some_value, do:
if(some_value, do: :token
if(some_value, do: 1 + val
all return:
if(some_value, do: __cursor__())
For multi-line blocks, all previous lines are preserved.
The AST returned by this function is not safe to evaluate but
it can be analyzed and expanded.
## Examples
Function call:
iex> Code.Fragment.container_cursor_to_quoted("max(some_value, ")
{:ok, {:max, [line: 1], [{:some_value, [line: 1], nil}, {:__cursor__, [line: 1], []}]}}
Containers (for example, a list):
iex> Code.Fragment.container_cursor_to_quoted("[some, value")
{:ok, [{:some, [line: 1], nil}, {:__cursor__, [line: 1], []}]}
If an expression is complete, then the whole expression is discarded
and only the parent is returned:
iex> Code.Fragment.container_cursor_to_quoted("if(is_atom(var)")
{:ok, {:if, [line: 1], [{:__cursor__, [line: 1], []}]}}
this means complete expressions themselves return only the cursor:
iex> Code.Fragment.container_cursor_to_quoted("if(is_atom(var))")
{:ok, {:__cursor__, [line: 1], []}}
Operators are also included from Elixir v1.15:
iex> Code.Fragment.container_cursor_to_quoted("foo +")
{:ok, {:+, [line: 1], [{:foo, [line: 1], nil}, {:__cursor__, [line: 1], []}]}}
In order to parse the left-side of `->` properly, which appears both
in anonymous functions and do-end blocks, the trailing fragment option
must be given with the rest of the contents:
iex> Code.Fragment.container_cursor_to_quoted("fn x", trailing_fragment: " -> :ok end")
{:ok, {:fn, [line: 1], [{:->, [line: 1], [[{:__cursor__, [line: 1], []}], :ok]}]}}
## Options
* `:file` - the filename to be reported in case of parsing errors.
Defaults to `"nofile"`.
* `:line` - the starting line of the string being parsed.
Defaults to `1`.
* `:column` - the starting column of the string being parsed.
Defaults to `1`.
* `:columns` - when `true`, attach a `:column` key to the quoted
metadata. Defaults to `false`.
* `:token_metadata` - when `true`, includes token-related
metadata in the expression AST, such as metadata for `do` and `end`
tokens, for closing tokens, end of expressions, as well as delimiters
for sigils. See `t:Macro.metadata/0`. Defaults to `false`.
* `:literal_encoder` - a function to encode literals in the AST.
See the documentation for `Code.string_to_quoted/2` for more information.
* `:trailing_fragment` (since v1.18.0) - the rest of the contents after
the cursor. This is necessary to correctly complete anonymous functions
and the left-hand side of `->`
"""
@doc since: "1.13.0"
@spec container_cursor_to_quoted(List.Chars.t(), container_cursor_to_quoted_opts()) ::
{:ok, Macro.t()} | {:error, {location :: keyword, binary | {binary, binary}, binary}}
def container_cursor_to_quoted(fragment, opts \\ []) do
{trailing_fragment, opts} = Keyword.pop(opts, :trailing_fragment)
opts = Keyword.take(opts, [:columns, :token_metadata, :literal_encoder])
opts = [check_terminators: {:cursor, []}, emit_warnings: false] ++ opts
file = Keyword.get(opts, :file, "nofile")
line = Keyword.get(opts, :line, 1)
column = Keyword.get(opts, :column, 1)
case :elixir_tokenizer.tokenize(to_charlist(fragment), line, column, opts) do
{:ok, line, column, _warnings, rev_tokens, rev_terminators}
when trailing_fragment == nil ->
{rev_tokens, rev_terminators} =
with [close, open, {_, _, :__cursor__} = cursor | rev_tokens] <- rev_tokens,
{_, [_ | after_fn]} <- Enum.split_while(rev_terminators, &(elem(&1, 0) != :fn)),
true <- maybe_missing_stab?(rev_tokens, false),
[_ | rev_tokens] <- Enum.drop_while(rev_tokens, &(elem(&1, 0) != :fn)) do
{[close, open, cursor | rev_tokens], after_fn}
else
_ -> {rev_tokens, rev_terminators}
end
tokens = reverse_tokens(line, column, rev_tokens, rev_terminators)
:elixir.tokens_to_quoted(tokens, file, opts)
{:ok, line, column, _warnings, rev_tokens, rev_terminators} ->
tokens =
with {before_start, [_ | _] = after_start} <-
Enum.split_while(rev_terminators, &(elem(&1, 0) not in [:do, :fn])),
true <- maybe_missing_stab?(rev_tokens, true),
opts =
Keyword.put(opts, :check_terminators, {:cursor, before_start}),
{:error, {meta, _, ~c"end"}, _rest, _warnings, trailing_rev_tokens} <-
:elixir_tokenizer.tokenize(to_charlist(trailing_fragment), line, column, opts) do
trailing_tokens =
reverse_tokens(meta[:line], meta[:column], trailing_rev_tokens, after_start)
# If the cursor has its own line, then we do not trim new lines trailing tokens.
# Otherwise we want to drop any newline so we drop the next tokens after eol.
trailing_tokens =
case rev_tokens do
[_close, _open, {_, _, :__cursor__}, {:eol, _} | _] -> trailing_tokens
_ -> Enum.drop_while(trailing_tokens, &match?({:eol, _}, &1))
end
Enum.reverse(rev_tokens, drop_tokens(trailing_tokens, 0))
else
_ -> reverse_tokens(line, column, rev_tokens, rev_terminators)
end
:elixir.tokens_to_quoted(tokens, file, opts)
{:error, info, _rest, _warnings, _so_far} ->
{:error, :elixir.format_token_error(info)}
end
end
defp reverse_tokens(line, column, tokens, terminators) do
{terminators, _} =
Enum.map_reduce(terminators, column, fn {start, _, _}, column ->
atom = :elixir_tokenizer.terminator(start)
{{atom, {line, column, nil}}, column + length(Atom.to_charlist(atom))}
end)
Enum.reverse(tokens, terminators)
end
# Otherwise we drop all tokens, trying to build a minimal AST
# for cursor completion.
defp drop_tokens([{:"}", _} | _] = tokens, 0), do: tokens
defp drop_tokens([{:"]", _} | _] = tokens, 0), do: tokens
defp drop_tokens([{:")", _} | _] = tokens, 0), do: tokens
defp drop_tokens([{:">>", _} | _] = tokens, 0), do: tokens
defp drop_tokens([{:end, _} | _] = tokens, 0), do: tokens
defp drop_tokens([{:",", _} | _] = tokens, 0), do: tokens
defp drop_tokens([{:";", _} | _] = tokens, 0), do: tokens
defp drop_tokens([{:eol, _} | _] = tokens, 0), do: tokens
defp drop_tokens([{:stab_op, _, :->} | _] = tokens, 0), do: tokens
defp drop_tokens([{:"}", _} | tokens], counter), do: drop_tokens(tokens, counter - 1)
defp drop_tokens([{:"]", _} | tokens], counter), do: drop_tokens(tokens, counter - 1)
defp drop_tokens([{:")", _} | tokens], counter), do: drop_tokens(tokens, counter - 1)
defp drop_tokens([{:">>", _} | tokens], counter), do: drop_tokens(tokens, counter - 1)
defp drop_tokens([{:end, _} | tokens], counter), do: drop_tokens(tokens, counter - 1)
defp drop_tokens([{:"{", _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
defp drop_tokens([{:"[", _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
defp drop_tokens([{:"(", _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
defp drop_tokens([{:"<<", _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
defp drop_tokens([{:fn, _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
defp drop_tokens([{:do, _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
defp drop_tokens([_ | tokens], counter), do: drop_tokens(tokens, counter)
defp drop_tokens([], _counter), do: []
defp maybe_missing_stab?([{:after, _} | _], _stab_choice?), do: true
defp maybe_missing_stab?([{:do, _} | _], _stab_choice?), do: true
defp maybe_missing_stab?([{:fn, _} | _], _stab_choice?), do: true
defp maybe_missing_stab?([{:else, _} | _], _stab_choice?), do: true
defp maybe_missing_stab?([{:catch, _} | _], _stab_choice?), do: true
defp maybe_missing_stab?([{:rescue, _} | _], _stab_choice?), do: true
defp maybe_missing_stab?([{:stab_op, _, :->} | _], stab_choice?), do: stab_choice?
defp maybe_missing_stab?([_ | tail], stab_choice?), do: maybe_missing_stab?(tail, stab_choice?)
defp maybe_missing_stab?([], _stab_choice?), do: false
end