diff options
author | José Valim <jose.valim@dashbit.co> | 2022-07-09 17:06:03 +0200 |
---|---|---|
committer | José Valim <jose.valim@dashbit.co> | 2022-07-09 17:06:04 +0200 |
commit | f49b3cbec3268a266d995d46fff4775faea5be4a (patch) | |
tree | 045d88fc9c9e2d3aa1a431e5b2c54afff9a853b0 /lib/elixir | |
parent | 897e64f5e57f3e156adb53d60e333ad7ef8424c0 (diff) | |
download | elixir-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.ex | 14 | ||||
-rw-r--r-- | lib/elixir/lib/macro.ex | 90 | ||||
-rw-r--r-- | lib/elixir/src/elixir_interpolation.erl | 2 | ||||
-rw-r--r-- | lib/elixir/src/elixir_tokenizer.erl | 16 | ||||
-rw-r--r-- | lib/elixir/test/elixir/code_fragment_test.exs | 7 |
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__()]") |