summaryrefslogtreecommitdiff
path: root/lib/elixir
diff options
context:
space:
mode:
authorJosé Valim <jose.valim@dashbit.co>2022-07-09 17:06:03 +0200
committerJosé Valim <jose.valim@dashbit.co>2022-07-09 17:06:04 +0200
commitf49b3cbec3268a266d995d46fff4775faea5be4a (patch)
tree045d88fc9c9e2d3aa1a431e5b2c54afff9a853b0 /lib/elixir
parent897e64f5e57f3e156adb53d60e333ad7ef8424c0 (diff)
downloadelixir-f49b3cbec3268a266d995d46fff4775faea5be4a.tar.gz
Add autocompletion of binary modifiers in IEx
Macro.path/2 was added as a helper function for traversing expressions and returning their context.
Diffstat (limited to 'lib/elixir')
-rw-r--r--lib/elixir/lib/code/fragment.ex14
-rw-r--r--lib/elixir/lib/macro.ex90
-rw-r--r--lib/elixir/src/elixir_interpolation.erl2
-rw-r--r--lib/elixir/src/elixir_tokenizer.erl16
-rw-r--r--lib/elixir/test/elixir/code_fragment_test.exs7
5 files changed, 121 insertions, 8 deletions
diff --git a/lib/elixir/lib/code/fragment.ex b/lib/elixir/lib/code/fragment.ex
index 0b635d120..e0b5d00bd 100644
--- a/lib/elixir/lib/code/fragment.ex
+++ b/lib/elixir/lib/code/fragment.ex
@@ -861,7 +861,7 @@ defmodule Code.Fragment do
max(some_value, 1 + another_val
max(some_value, 1 |> some_fun() |> another_fun
- On the other hand, tuples, lists, maps, etc all retain the
+ On the other hand, tuples, lists, maps, and binaries all retain the
cursor position:
max(some_value, [1, 2,
@@ -885,9 +885,21 @@ defmodule Code.Fragment do
## 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], []}]}
+
+ For binaries, the `::` is exclusively kept as an operator:
+
+ iex> Code.Fragment.container_cursor_to_quoted("<<some::integer")
+ {:ok, {:<<>>, [line: 1], [{:"::", [line: 1], [{:some, [line: 1], nil}, {:__cursor__, [line: 1], []}]}]}}
+
## Options
* `:file` - the filename to be reported in case of parsing errors.
diff --git a/lib/elixir/lib/macro.ex b/lib/elixir/lib/macro.ex
index 298cf54cd..40d26ca74 100644
--- a/lib/elixir/lib/macro.ex
+++ b/lib/elixir/lib/macro.ex
@@ -356,6 +356,96 @@ defmodule Macro do
def generate_arguments(amount, context), do: generate_arguments(amount, context, &var/2)
@doc """
+ Returns the path to the node in `ast` which `fun` returns true.
+
+ The path is a list, starting with the node in which `fun` returns
+ true, followed by all of its parents.
+
+ Computing the path can be an efficient operation when you want
+ to find a particular node in the AST within its context and then
+ assert something about it.
+
+ ## Examples
+
+ iex> Macro.path(quote(do: [1, 2, 3]), & &1 == 3)
+ [3, [1, 2, 3]]
+
+ iex> Macro.path(quote(do: Foo.bar(3)), & &1 == 3)
+ [3, quote(do: Foo.bar(3))]
+
+ iex> Macro.path(quote(do: %{foo: [bar: :baz]}), & &1 == :baz)
+ [
+ :baz,
+ {:bar, :baz},
+ [bar: :baz],
+ {:foo, [bar: :baz]},
+ {:%{}, [], [foo: [bar: :baz]]}
+ ]
+
+ """
+ @doc since: "1.14.0"
+ def path(ast, fun) when is_function(fun, 1) do
+ path(ast, [], fun)
+ end
+
+ defp path({form, _, args} = ast, acc, fun) when is_atom(form) do
+ acc = [ast | acc]
+
+ if fun.(ast) do
+ acc
+ else
+ path_args(args, acc, fun)
+ end
+ end
+
+ defp path({form, _meta, args} = ast, acc, fun) do
+ acc = [ast | acc]
+
+ if fun.(ast) do
+ acc
+ else
+ path(form, acc, fun) || path_args(args, acc, fun)
+ end
+ end
+
+ defp path({left, right} = ast, acc, fun) do
+ acc = [ast | acc]
+
+ if fun.(ast) do
+ acc
+ else
+ path(left, acc, fun) || path(right, acc, fun)
+ end
+ end
+
+ defp path(list, acc, fun) when is_list(list) do
+ acc = [list | acc]
+
+ if fun.(list) do
+ acc
+ else
+ path_list(list, acc, fun)
+ end
+ end
+
+ defp path(ast, acc, fun) do
+ if fun.(ast) do
+ [ast | acc]
+ end
+ end
+
+ defp path_args(atom, _acc, _fun) when is_atom(atom), do: nil
+ defp path_args(list, acc, fun) when is_list(list), do: path_list(list, acc, fun)
+
+ defp path_list([], _acc, _fun) do
+ nil
+ end
+
+ defp path_list([arg | args], acc, fun) do
+ path(arg, acc, fun) || path_list(args, acc, fun)
+ end
+
+ @doc """
Generates AST nodes for a given number of required argument
variables using `Macro.unique_var/2`.
diff --git a/lib/elixir/src/elixir_interpolation.erl b/lib/elixir/src/elixir_interpolation.erl
index 2fea35a62..7c7533594 100644
--- a/lib/elixir/src/elixir_interpolation.erl
+++ b/lib/elixir/src/elixir_interpolation.erl
@@ -51,7 +51,7 @@ extract([$#, ${ | Rest], Buffer, Output, Line, Column, Scope, true, Last) ->
{error, Reason, _, _, _} ->
{error, Reason};
{ok, EndLine, EndColumn, Warnings, Tokens} when Scope#elixir_tokenizer.cursor_completion /= false ->
- NewScope = Scope#elixir_tokenizer{warnings=Warnings, cursor_completion=terminators},
+ NewScope = Scope#elixir_tokenizer{warnings=Warnings, cursor_completion=noprune},
Output2 = build_interpol(Line, Column, EndLine, EndColumn, Tokens, Output1),
extract([], [], Output2, EndLine, EndColumn, NewScope, true, Last);
{ok, _, _, _, _} ->
diff --git a/lib/elixir/src/elixir_tokenizer.erl b/lib/elixir/src/elixir_tokenizer.erl
index 108c9801a..c10082fe1 100644
--- a/lib/elixir/src/elixir_tokenizer.erl
+++ b/lib/elixir/src/elixir_tokenizer.erl
@@ -114,7 +114,7 @@ tokenize(String, Line, Column, Opts) ->
({check_terminators, false}, Acc) ->
Acc#elixir_tokenizer{terminators=none};
({cursor_completion, true}, Acc) ->
- Acc#elixir_tokenizer{cursor_completion=cursor_and_terminators};
+ Acc#elixir_tokenizer{cursor_completion=prune_and_cursor};
({existing_atoms_only, ExistingAtomsOnly}, Acc) when is_boolean(ExistingAtomsOnly) ->
Acc#elixir_tokenizer{existing_atoms_only=ExistingAtomsOnly};
({static_atoms_encoder, StaticAtomsEncoder}, Acc) when is_function(StaticAtomsEncoder) ->
@@ -134,8 +134,8 @@ tokenize(String, Line, Column, Opts) ->
tokenize(String, Line, Opts) ->
tokenize(String, Line, 1, Opts).
-tokenize([], Line, Column, #elixir_tokenizer{ascii_identifiers_only=Ascii, cursor_completion=Cursor} = Scope, Tokens) when Cursor /= false ->
- #elixir_tokenizer{terminators=Terminators, warnings=Warnings} = Scope,
+tokenize([], Line, Column, #elixir_tokenizer{cursor_completion=Cursor} = Scope, Tokens) when Cursor /= false ->
+ #elixir_tokenizer{ascii_identifiers_only=Ascii, terminators=Terminators, warnings=Warnings} = Scope,
{CursorColumn, CursorTerminators, CursorTokens} =
add_cursor(Line, Column, Cursor, Terminators, Tokens),
@@ -151,7 +151,8 @@ tokenize([], EndLine, Column, #elixir_tokenizer{terminators=[{Start, StartLine,
Formatted = io_lib:format(Message, [End, Start, StartLine]),
error({EndLine, Column, [Formatted, Hint], []}, [], Scope, Tokens);
-tokenize([], Line, Column, #elixir_tokenizer{ascii_identifiers_only=Ascii, warnings=Warnings}, Tokens) ->
+tokenize([], Line, Column, #elixir_tokenizer{} = Scope, Tokens) ->
+ #elixir_tokenizer{ascii_identifiers_only=Ascii, warnings=Warnings} = Scope,
AllWarnings = maybe_unicode_lint_warnings(Ascii, Tokens, Warnings),
{ok, Line, Column, AllWarnings, lists:reverse(Tokens)};
@@ -1664,9 +1665,9 @@ cursor_complete(Line, Column, Terminators, Tokens) ->
),
lists:reverse(AccTokens).
-add_cursor(_Line, Column, terminators, Terminators, Tokens) ->
+add_cursor(_Line, Column, noprune, Terminators, Tokens) ->
{Column, Terminators, Tokens};
-add_cursor(Line, Column, cursor_and_terminators, Terminators, Tokens) ->
+add_cursor(Line, Column, prune_and_cursor, Terminators, Tokens) ->
{PrunedTokens, PrunedTerminators} = prune_tokens(Tokens, [], Terminators),
CursorTokens = [
{')', {Line, Column + 11, nil}},
@@ -1726,6 +1727,9 @@ prune_tokens([{kw_identifier_safe, _, _} | _] = Tokens, [], Terminators) ->
{Tokens, Terminators};
prune_tokens([{kw_identifier_unsafe, _, _} | _] = Tokens, [], Terminators) ->
{Tokens, Terminators};
+%%% we usually skip operators, except these contextual ones
+prune_tokens([{type_op, _, '::'} | _] = Tokens, [], [{'<<', _, _} | _] = Terminators) ->
+ {Tokens, Terminators};
%%% or we traverse until the end.
prune_tokens([_ | Tokens], Opener, Terminators) ->
prune_tokens(Tokens, Opener, Terminators);
diff --git a/lib/elixir/test/elixir/code_fragment_test.exs b/lib/elixir/test/elixir/code_fragment_test.exs
index 16e688572..30457ae24 100644
--- a/lib/elixir/test/elixir/code_fragment_test.exs
+++ b/lib/elixir/test/elixir/code_fragment_test.exs
@@ -1031,6 +1031,13 @@ defmodule CodeFragmentTest do
assert cc2q("%Foo{bar: baz,") == s2q("%Foo{bar: baz, __cursor__()}")
end
+ test "binaries" do
+ assert cc2q("<<") == s2q("<<__cursor__()>>")
+ assert cc2q("<<foo") == s2q("<<__cursor__()>>")
+ assert cc2q("<<foo, bar") == s2q("<<foo, __cursor__()>>")
+ assert cc2q("<<foo, bar::baz") == s2q("<<foo, bar::__cursor__()>>")
+ end
+
test "removes tokens until opening" do
assert cc2q("(123") == s2q("(__cursor__())")
assert cc2q("[foo") == s2q("[__cursor__()]")