From 2e5c871a74d9e4e6b61fcc63569d5875641e4ca3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 8 Apr 2025 11:19:36 +0200 Subject: [PATCH 1/2] Track block keywords --- lib/elixir/lib/code/fragment.ex | 60 ++++++++++++--- lib/elixir/test/elixir/code_fragment_test.exs | 74 ++++++++++++++++++- 2 files changed, 122 insertions(+), 12 deletions(-) diff --git a/lib/elixir/lib/code/fragment.ex b/lib/elixir/lib/code/fragment.ex index 5e36dda0c59..bda1f8253f5 100644 --- a/lib/elixir/lib/code/fragment.ex +++ b/lib/elixir/lib/code/fragment.ex @@ -46,6 +46,11 @@ defmodule Code.Fragment do or `{:local_or_var, charlist}` and `charlist` is a static part Examples are `__MODULE__.Submodule` or `@hello.Submodule` + * `:block_keyword_or_binary_operator` - may be a block keyword (do, end, after, + catch, else, rescue) or a binary operator + + * `{:block_keyword, charlist}` - the context is a block keyword + * `{: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` @@ -139,6 +144,8 @@ defmodule Code.Fragment do @spec cursor_context(List.Chars.t(), keyword()) :: {:alias, charlist} | {:alias, inside_alias, charlist} + | :block_keyword_or_binary_operator + | {:block_keyword, charlist} | {:dot, inside_dot, charlist} | {:dot_arity, inside_dot, charlist} | {:dot_call, inside_dot, charlist} @@ -188,15 +195,15 @@ defmodule Code.Fragment do cursor_context(to_charlist(other), opts) end - @operators ~c"\\<>+-*/:=|&~^%!" - @starter_punctuation ~c",([{;" - @non_starter_punctuation ~c")]}\"'.$" + @operators ~c"\\<>+-*/:=|&~^%!$" + @starting_punctuation ~c",([{;" + @closing_punctuation ~c")]}\"'" @space ~c"\t\s" @trailing_identifier ~c"?!" @tilde_op_prefix ~c"<=~" @non_identifier @trailing_identifier ++ - @operators ++ @starter_punctuation ++ @non_starter_punctuation ++ @space + @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 @@ -226,11 +233,11 @@ defmodule Code.Fragment do # A local arity definition [?/ | rest] -> arity_to_cursor_context(strip_spaces(rest, spaces + 1)) # Starting a new expression - [h | _] when h in @starter_punctuation -> {:expr, 0} - # It is a local or remote call without parens - rest when spaces > 0 -> call_to_cursor_context({rest, spaces}) + [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, 0, false) + _ -> identifier_to_cursor_context(reverse, spaces, false) end end @@ -269,6 +276,14 @@ defmodule Code.Fragment do end end + defp closing_or_call_to_cursor_context({reverse, spaces}) do + if closing?(reverse) do + {:block_keyword_or_binary_operator, 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: {{:local_or_var, ~c"..."}, n + 3} defp identifier_to_cursor_context([?., ?: | _], n, _), do: {{:unquoted_atom, ~c"."}, n + 2} @@ -320,8 +335,11 @@ defmodule Code.Fragment do {~c"." ++ rest, count} when rest == [] or hd(rest) != ?. -> dot(rest, count + 1, acc) - _ -> - {{:local_or_var, acc}, count} + {rest, rest_count} -> + response = + if rest_count > count and closing?(rest), do: :block_keyword, else: :local_or_var + + {{response, acc}, count} end {:capture_arg, acc, count} -> @@ -329,6 +347,28 @@ defmodule Code.Fragment do 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, []) diff --git a/lib/elixir/test/elixir/code_fragment_test.exs b/lib/elixir/test/elixir/code_fragment_test.exs index 60016127059..9f9e363cd94 100644 --- a/lib/elixir/test/elixir/code_fragment_test.exs +++ b/lib/elixir/test/elixir/code_fragment_test.exs @@ -240,6 +240,9 @@ defmodule CodeFragmentTest do assert CF.cursor_context("Hello::Wor") == {:alias, ~c"Wor"} assert CF.cursor_context("Hello..Wor") == {:alias, ~c"Wor"} + assert CF.cursor_context("hello.World") == + {:alias, {:local_or_var, ~c"hello"}, ~c"World"} + assert CF.cursor_context("__MODULE__.Wor") == {:alias, {:local_or_var, ~c"__MODULE__"}, ~c"Wor"} @@ -368,6 +371,75 @@ defmodule CodeFragmentTest do assert CF.cursor_context("@hello_wo") == {:module_attribute, ~c"hello_wo"} end + test "keyword or binary operator" do + # Literals + assert CF.cursor_context("Foo.Bar ") == :block_keyword_or_binary_operator + assert CF.cursor_context("Foo ") == :block_keyword_or_binary_operator + assert CF.cursor_context(":foo ") == :block_keyword_or_binary_operator + assert CF.cursor_context("123 ") == :block_keyword_or_binary_operator + assert CF.cursor_context("nil ") == :block_keyword_or_binary_operator + assert CF.cursor_context("true ") == :block_keyword_or_binary_operator + assert CF.cursor_context("false ") == :block_keyword_or_binary_operator + assert CF.cursor_context("\"foo\" ") == :block_keyword_or_binary_operator + assert CF.cursor_context("'foo' ") == :block_keyword_or_binary_operator + + # Containers + assert CF.cursor_context("(foo) ") == :block_keyword_or_binary_operator + assert CF.cursor_context("[foo] ") == :block_keyword_or_binary_operator + assert CF.cursor_context("{foo} ") == :block_keyword_or_binary_operator + assert CF.cursor_context("<> ") == :block_keyword_or_binary_operator + + # False positives + assert CF.cursor_context("foo ~>> ") == {:operator_call, ~c"~>>"} + assert CF.cursor_context("foo >>> ") == {:operator_call, ~c">>>"} + end + + test "keyword from keyword or binary operator" do + # Literals + assert CF.cursor_context("Foo.Bar d") == {:block_keyword, ~c"d"} + assert CF.cursor_context("Foo d") == {:block_keyword, ~c"d"} + assert CF.cursor_context(":foo d") == {:block_keyword, ~c"d"} + assert CF.cursor_context("123 d") == {:block_keyword, ~c"d"} + assert CF.cursor_context("nil d") == {:block_keyword, ~c"d"} + assert CF.cursor_context("true d") == {:block_keyword, ~c"d"} + assert CF.cursor_context("false d") == {:block_keyword, ~c"d"} + assert CF.cursor_context("\"foo\" d") == {:block_keyword, ~c"d"} + assert CF.cursor_context("'foo' d") == {:block_keyword, ~c"d"} + + # Containers + assert CF.cursor_context("(foo) d") == {:block_keyword, ~c"d"} + assert CF.cursor_context("[foo] d") == {:block_keyword, ~c"d"} + assert CF.cursor_context("{foo} d") == {:block_keyword, ~c"d"} + assert CF.cursor_context("<> d") == {:block_keyword, ~c"d"} + + # False positives + assert CF.cursor_context("foo ~>> d") == {:local_or_var, ~c"d"} + assert CF.cursor_context("foo >>> d") == {:local_or_var, ~c"d"} + end + + test "operator from keyword or binary operator" do + # Literals + assert CF.cursor_context("Foo.Bar +") == {:operator, ~c"+"} + assert CF.cursor_context("Foo +") == {:operator, ~c"+"} + assert CF.cursor_context(":foo +") == {:operator, ~c"+"} + assert CF.cursor_context("123 +") == {:operator, ~c"+"} + assert CF.cursor_context("nil +") == {:operator, ~c"+"} + assert CF.cursor_context("true +") == {:operator, ~c"+"} + assert CF.cursor_context("false +") == {:operator, ~c"+"} + assert CF.cursor_context("\"foo\" +") == {:operator, ~c"+"} + assert CF.cursor_context("'foo' +") == {:operator, ~c"+"} + + # Containers + assert CF.cursor_context("(foo) +") == {:operator, ~c"+"} + assert CF.cursor_context("[foo] +") == {:operator, ~c"+"} + assert CF.cursor_context("{foo} +") == {:operator, ~c"+"} + assert CF.cursor_context("<> +") == {:operator, ~c"+"} + + # False positives + assert CF.cursor_context("foo ~>> +") == {:operator, ~c"+"} + assert CF.cursor_context("foo >>> +") == {:operator, ~c"+"} + end + test "none" do # Punctuation assert CF.cursor_context(")") == :none @@ -399,8 +471,6 @@ defmodule CodeFragmentTest do assert CF.cursor_context("HelloWór") == :none assert CF.cursor_context("@Hello") == :none assert CF.cursor_context("Hello(") == :none - assert CF.cursor_context("Hello ") == :none - assert CF.cursor_context("hello.World") == {:alias, {:local_or_var, ~c"hello"}, ~c"World"} # Identifier assert CF.cursor_context("foo@bar") == :none From e15220e1b25a0440c82b6b684f7751f328a7b58b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 8 Apr 2025 11:46:28 +0200 Subject: [PATCH 2/2] Implement it on IEx autocomplete --- lib/elixir/lib/code/fragment.ex | 13 +++-- lib/elixir/test/elixir/code_fragment_test.exs | 53 ++++++++++--------- lib/iex/lib/iex/autocomplete.ex | 26 +++++++++ lib/iex/test/iex/autocomplete_test.exs | 15 ++++++ 4 files changed, 74 insertions(+), 33 deletions(-) diff --git a/lib/elixir/lib/code/fragment.ex b/lib/elixir/lib/code/fragment.ex index bda1f8253f5..431951e3f13 100644 --- a/lib/elixir/lib/code/fragment.ex +++ b/lib/elixir/lib/code/fragment.ex @@ -46,11 +46,9 @@ defmodule Code.Fragment do or `{:local_or_var, charlist}` and `charlist` is a static part Examples are `__MODULE__.Submodule` or `@hello.Submodule` - * `:block_keyword_or_binary_operator` - may be a block keyword (do, end, after, + * `{:block_keyword_or_binary_operator, charlist}` - may be a block keyword (do, end, after, catch, else, rescue) or a binary operator - * `{:block_keyword, charlist}` - the context is a block keyword - * `{: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` @@ -144,8 +142,7 @@ defmodule Code.Fragment do @spec cursor_context(List.Chars.t(), keyword()) :: {:alias, charlist} | {:alias, inside_alias, charlist} - | :block_keyword_or_binary_operator - | {:block_keyword, charlist} + | {:block_keyword_or_binary_operator, charlist} | {:dot, inside_dot, charlist} | {:dot_arity, inside_dot, charlist} | {:dot_call, inside_dot, charlist} @@ -278,7 +275,7 @@ defmodule Code.Fragment do defp closing_or_call_to_cursor_context({reverse, spaces}) do if closing?(reverse) do - {:block_keyword_or_binary_operator, 0} + {{:block_keyword_or_binary_operator, ~c""}, 0} else call_to_cursor_context({reverse, spaces}) end @@ -337,7 +334,9 @@ defmodule Code.Fragment do {rest, rest_count} -> response = - if rest_count > count and closing?(rest), do: :block_keyword, else: :local_or_var + if rest_count > count and closing?(rest), + do: :block_keyword_or_binary_operator, + else: :local_or_var {{response, acc}, count} end diff --git a/lib/elixir/test/elixir/code_fragment_test.exs b/lib/elixir/test/elixir/code_fragment_test.exs index 9f9e363cd94..cd046019674 100644 --- a/lib/elixir/test/elixir/code_fragment_test.exs +++ b/lib/elixir/test/elixir/code_fragment_test.exs @@ -373,21 +373,21 @@ defmodule CodeFragmentTest do test "keyword or binary operator" do # Literals - assert CF.cursor_context("Foo.Bar ") == :block_keyword_or_binary_operator - assert CF.cursor_context("Foo ") == :block_keyword_or_binary_operator - assert CF.cursor_context(":foo ") == :block_keyword_or_binary_operator - assert CF.cursor_context("123 ") == :block_keyword_or_binary_operator - assert CF.cursor_context("nil ") == :block_keyword_or_binary_operator - assert CF.cursor_context("true ") == :block_keyword_or_binary_operator - assert CF.cursor_context("false ") == :block_keyword_or_binary_operator - assert CF.cursor_context("\"foo\" ") == :block_keyword_or_binary_operator - assert CF.cursor_context("'foo' ") == :block_keyword_or_binary_operator + assert CF.cursor_context("Foo.Bar ") == {:block_keyword_or_binary_operator, ~c""} + assert CF.cursor_context("Foo ") == {:block_keyword_or_binary_operator, ~c""} + assert CF.cursor_context(":foo ") == {:block_keyword_or_binary_operator, ~c""} + assert CF.cursor_context("123 ") == {:block_keyword_or_binary_operator, ~c""} + assert CF.cursor_context("nil ") == {:block_keyword_or_binary_operator, ~c""} + assert CF.cursor_context("true ") == {:block_keyword_or_binary_operator, ~c""} + assert CF.cursor_context("false ") == {:block_keyword_or_binary_operator, ~c""} + assert CF.cursor_context("\"foo\" ") == {:block_keyword_or_binary_operator, ~c""} + assert CF.cursor_context("'foo' ") == {:block_keyword_or_binary_operator, ~c""} # Containers - assert CF.cursor_context("(foo) ") == :block_keyword_or_binary_operator - assert CF.cursor_context("[foo] ") == :block_keyword_or_binary_operator - assert CF.cursor_context("{foo} ") == :block_keyword_or_binary_operator - assert CF.cursor_context("<> ") == :block_keyword_or_binary_operator + assert CF.cursor_context("(foo) ") == {:block_keyword_or_binary_operator, ~c""} + assert CF.cursor_context("[foo] ") == {:block_keyword_or_binary_operator, ~c""} + assert CF.cursor_context("{foo} ") == {:block_keyword_or_binary_operator, ~c""} + assert CF.cursor_context("<> ") == {:block_keyword_or_binary_operator, ~c""} # False positives assert CF.cursor_context("foo ~>> ") == {:operator_call, ~c"~>>"} @@ -396,21 +396,22 @@ defmodule CodeFragmentTest do test "keyword from keyword or binary operator" do # Literals - assert CF.cursor_context("Foo.Bar d") == {:block_keyword, ~c"d"} - assert CF.cursor_context("Foo d") == {:block_keyword, ~c"d"} - assert CF.cursor_context(":foo d") == {:block_keyword, ~c"d"} - assert CF.cursor_context("123 d") == {:block_keyword, ~c"d"} - assert CF.cursor_context("nil d") == {:block_keyword, ~c"d"} - assert CF.cursor_context("true d") == {:block_keyword, ~c"d"} - assert CF.cursor_context("false d") == {:block_keyword, ~c"d"} - assert CF.cursor_context("\"foo\" d") == {:block_keyword, ~c"d"} - assert CF.cursor_context("'foo' d") == {:block_keyword, ~c"d"} + assert CF.cursor_context("Foo.Bar do") == {:block_keyword_or_binary_operator, ~c"do"} + assert CF.cursor_context("Foo.Bar d") == {:block_keyword_or_binary_operator, ~c"d"} + assert CF.cursor_context("Foo d") == {:block_keyword_or_binary_operator, ~c"d"} + assert CF.cursor_context(":foo d") == {:block_keyword_or_binary_operator, ~c"d"} + assert CF.cursor_context("123 d") == {:block_keyword_or_binary_operator, ~c"d"} + assert CF.cursor_context("nil d") == {:block_keyword_or_binary_operator, ~c"d"} + assert CF.cursor_context("true d") == {:block_keyword_or_binary_operator, ~c"d"} + assert CF.cursor_context("false d") == {:block_keyword_or_binary_operator, ~c"d"} + assert CF.cursor_context("\"foo\" d") == {:block_keyword_or_binary_operator, ~c"d"} + assert CF.cursor_context("'foo' d") == {:block_keyword_or_binary_operator, ~c"d"} # Containers - assert CF.cursor_context("(foo) d") == {:block_keyword, ~c"d"} - assert CF.cursor_context("[foo] d") == {:block_keyword, ~c"d"} - assert CF.cursor_context("{foo} d") == {:block_keyword, ~c"d"} - assert CF.cursor_context("<> d") == {:block_keyword, ~c"d"} + assert CF.cursor_context("(foo) d") == {:block_keyword_or_binary_operator, ~c"d"} + assert CF.cursor_context("[foo] d") == {:block_keyword_or_binary_operator, ~c"d"} + assert CF.cursor_context("{foo} d") == {:block_keyword_or_binary_operator, ~c"d"} + assert CF.cursor_context("<> d") == {:block_keyword_or_binary_operator, ~c"d"} # False positives assert CF.cursor_context("foo ~>> d") == {:local_or_var, ~c"d"} diff --git a/lib/iex/lib/iex/autocomplete.ex b/lib/iex/lib/iex/autocomplete.ex index 5263bd5edaf..830ae173322 100644 --- a/lib/iex/lib/iex/autocomplete.ex +++ b/lib/iex/lib/iex/autocomplete.ex @@ -22,6 +22,23 @@ defmodule IEx.Autocomplete do %{kind: :variable, name: "utf32"} ] + block_keywords = + for block_keyword <- ~w(do end after catch else rescue) do + %{kind: :block_keyword, name: block_keyword} + end + + binary_operators = + for operator <- + ["**", "*", "/", "+", "-", "++", "--", "+++", "---", "..", "<>"] ++ + ["in", "not in", "|>", "<<<", ">>>", "<<~", "~>>", "<~", "~>", "<~>"] ++ + ["<", ">", "<=", ">=", "==", "!=", "=~", "===", "!=="] ++ + ["&&", "&&&", "and", "||", "|||", "or"] ++ + ["=", "=>", "|", "::", "when", "<-", "\\\\"] do + %{kind: :export, name: operator, arity: 2} + end + + @block_keyword_or_binary_operator block_keywords ++ Enum.sort(binary_operators) + @alias_only_atoms ~w(alias import require)a @alias_only_charlists ~w(alias import require)c @@ -63,6 +80,9 @@ defmodule IEx.Autocomplete do {:unquoted_atom, unquoted_atom} -> expand_erlang_modules(List.to_string(unquoted_atom)) + {:block_keyword_or_binary_operator, hint} -> + filter_and_format_expansion(@block_keyword_or_binary_operator, List.to_string(hint)) + expansion when helper == ?b -> expand_typespecs(expansion, shell, &get_module_callbacks/1) @@ -519,6 +539,12 @@ defmodule IEx.Autocomplete do ## Formatting + defp filter_and_format_expansion(results, hint) do + results + |> Enum.filter(&String.starts_with?(&1.name, hint)) + |> format_expansion(hint) + end + defp format_expansion([], _) do no() end diff --git a/lib/iex/test/iex/autocomplete_test.exs b/lib/iex/test/iex/autocomplete_test.exs index 2a8949cee3d..2323191b0b8 100644 --- a/lib/iex/test/iex/autocomplete_test.exs +++ b/lib/iex/test/iex/autocomplete_test.exs @@ -184,6 +184,21 @@ defmodule IEx.AutocompleteTest do assert expand(~c"IEx.Xyz") == {:no, ~c"", []} end + test "block keywords" do + assert expand(~c"if true do") == {:yes, ~c"", [~c"do"]} + assert expand(~c"if true a") == {:yes, ~c"", [~c"after", ~c"and/2"]} + assert expand(~c"if true d") == {:yes, ~c"o", []} + assert expand(~c"if true e") == {:yes, ~c"", [~c"end", ~c"else"]} + end + + test "block keywords or operators" do + {:yes, ~c"", hints} = expand(~c"if true ") + assert ~c"do" in hints + assert ~c"else" in hints + assert ~c"+/2" in hints + assert ~c"and/2" in hints + end + test "function completion" do assert expand(~c"System.ve") == {:yes, ~c"rsion", []} assert expand(~c":ets.fun2") == {:yes, ~c"ms", []}