Skip to content

Commit 32f7646

Browse files
authored
Add block_keyword_or_binary_operator to Code.Fragment (#14414)
1 parent 9449df8 commit 32f7646

File tree

4 files changed

+163
-12
lines changed

4 files changed

+163
-12
lines changed

Diff for: lib/elixir/lib/code/fragment.ex

+49-10
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ defmodule Code.Fragment do
4646
or `{:local_or_var, charlist}` and `charlist` is a static part
4747
Examples are `__MODULE__.Submodule` or `@hello.Submodule`
4848
49+
* `{:block_keyword_or_binary_operator, charlist}` - may be a block keyword (do, end, after,
50+
catch, else, rescue) or a binary operator
51+
4952
* `{:dot, inside_dot, charlist}` - the context is a dot
5053
where `inside_dot` is either a `{:var, charlist}`, `{:alias, charlist}`,
5154
`{:module_attribute, charlist}`, `{:unquoted_atom, charlist}` or a `dot`
@@ -139,6 +142,7 @@ defmodule Code.Fragment do
139142
@spec cursor_context(List.Chars.t(), keyword()) ::
140143
{:alias, charlist}
141144
| {:alias, inside_alias, charlist}
145+
| {:block_keyword_or_binary_operator, charlist}
142146
| {:dot, inside_dot, charlist}
143147
| {:dot_arity, inside_dot, charlist}
144148
| {:dot_call, inside_dot, charlist}
@@ -188,15 +192,15 @@ defmodule Code.Fragment do
188192
cursor_context(to_charlist(other), opts)
189193
end
190194

191-
@operators ~c"\\<>+-*/:=|&~^%!"
192-
@starter_punctuation ~c",([{;"
193-
@non_starter_punctuation ~c")]}\"'.$"
195+
@operators ~c"\\<>+-*/:=|&~^%!$"
196+
@starting_punctuation ~c",([{;"
197+
@closing_punctuation ~c")]}\"'"
194198
@space ~c"\t\s"
195199
@trailing_identifier ~c"?!"
196200
@tilde_op_prefix ~c"<=~"
197201

198202
@non_identifier @trailing_identifier ++
199-
@operators ++ @starter_punctuation ++ @non_starter_punctuation ++ @space
203+
@operators ++ @starting_punctuation ++ @closing_punctuation ++ @space ++ [?.]
200204

201205
@textual_operators ~w(when not and or in)c
202206
@keywords ~w(do end after else catch rescue fn true false nil)c
@@ -226,11 +230,11 @@ defmodule Code.Fragment do
226230
# A local arity definition
227231
[?/ | rest] -> arity_to_cursor_context(strip_spaces(rest, spaces + 1))
228232
# Starting a new expression
229-
[h | _] when h in @starter_punctuation -> {:expr, 0}
230-
# It is a local or remote call without parens
231-
rest when spaces > 0 -> call_to_cursor_context({rest, spaces})
233+
[h | _] when h in @starting_punctuation -> {:expr, 0}
234+
# It is keyword, binary operator, a local or remote call without parens
235+
rest when spaces > 0 -> closing_or_call_to_cursor_context({rest, spaces})
232236
# It is an identifier
233-
_ -> identifier_to_cursor_context(reverse, 0, false)
237+
_ -> identifier_to_cursor_context(reverse, spaces, false)
234238
end
235239
end
236240

@@ -269,6 +273,14 @@ defmodule Code.Fragment do
269273
end
270274
end
271275

276+
defp closing_or_call_to_cursor_context({reverse, spaces}) do
277+
if closing?(reverse) do
278+
{{:block_keyword_or_binary_operator, ~c""}, 0}
279+
else
280+
call_to_cursor_context({reverse, spaces})
281+
end
282+
end
283+
272284
defp identifier_to_cursor_context([?., ?., ?: | _], n, _), do: {{:unquoted_atom, ~c".."}, n + 3}
273285
defp identifier_to_cursor_context([?., ?., ?. | _], n, _), do: {{:local_or_var, ~c"..."}, n + 3}
274286
defp identifier_to_cursor_context([?., ?: | _], n, _), do: {{:unquoted_atom, ~c"."}, n + 2}
@@ -320,15 +332,42 @@ defmodule Code.Fragment do
320332
{~c"." ++ rest, count} when rest == [] or hd(rest) != ?. ->
321333
dot(rest, count + 1, acc)
322334

323-
_ ->
324-
{{:local_or_var, acc}, count}
335+
{rest, rest_count} ->
336+
response =
337+
if rest_count > count and closing?(rest),
338+
do: :block_keyword_or_binary_operator,
339+
else: :local_or_var
340+
341+
{{response, acc}, count}
325342
end
326343

327344
{:capture_arg, acc, count} ->
328345
{{:capture_arg, acc}, count}
329346
end
330347
end
331348

349+
# If it is a closing punctuation
350+
defp closing?([h | _]) when h in @closing_punctuation, do: true
351+
# Closing bitstring (but deal with operators)
352+
defp closing?([?>, ?> | rest]), do: rest == [] or hd(rest) not in [?>, ?~]
353+
# Keywords
354+
defp closing?(rest) do
355+
case split_non_identifier(rest, []) do
356+
{~c"nil", _} -> true
357+
{~c"true", _} -> true
358+
{~c"false", _} -> true
359+
{[digit | _], _} when digit in ?0..?9 -> true
360+
{[upper | _], _} when upper in ?A..?Z -> true
361+
{[_ | _], [?: | rest]} -> rest == [] or hd(rest) != ?:
362+
{_, _} -> false
363+
end
364+
end
365+
366+
defp split_non_identifier([h | t], acc) when h not in @non_identifier,
367+
do: split_non_identifier(t, [h | acc])
368+
369+
defp split_non_identifier(rest, acc), do: {acc, rest}
370+
332371
defp identifier([?? | rest], count), do: check_identifier(rest, count + 1, [??])
333372
defp identifier([?! | rest], count), do: check_identifier(rest, count + 1, [?!])
334373
defp identifier(rest, count), do: check_identifier(rest, count, [])

Diff for: lib/elixir/test/elixir/code_fragment_test.exs

+73-2
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,9 @@ defmodule CodeFragmentTest do
240240
assert CF.cursor_context("Hello::Wor") == {:alias, ~c"Wor"}
241241
assert CF.cursor_context("Hello..Wor") == {:alias, ~c"Wor"}
242242

243+
assert CF.cursor_context("hello.World") ==
244+
{:alias, {:local_or_var, ~c"hello"}, ~c"World"}
245+
243246
assert CF.cursor_context("__MODULE__.Wor") ==
244247
{:alias, {:local_or_var, ~c"__MODULE__"}, ~c"Wor"}
245248

@@ -368,6 +371,76 @@ defmodule CodeFragmentTest do
368371
assert CF.cursor_context("@hello_wo") == {:module_attribute, ~c"hello_wo"}
369372
end
370373

374+
test "keyword or binary operator" do
375+
# Literals
376+
assert CF.cursor_context("Foo.Bar ") == {:block_keyword_or_binary_operator, ~c""}
377+
assert CF.cursor_context("Foo ") == {:block_keyword_or_binary_operator, ~c""}
378+
assert CF.cursor_context(":foo ") == {:block_keyword_or_binary_operator, ~c""}
379+
assert CF.cursor_context("123 ") == {:block_keyword_or_binary_operator, ~c""}
380+
assert CF.cursor_context("nil ") == {:block_keyword_or_binary_operator, ~c""}
381+
assert CF.cursor_context("true ") == {:block_keyword_or_binary_operator, ~c""}
382+
assert CF.cursor_context("false ") == {:block_keyword_or_binary_operator, ~c""}
383+
assert CF.cursor_context("\"foo\" ") == {:block_keyword_or_binary_operator, ~c""}
384+
assert CF.cursor_context("'foo' ") == {:block_keyword_or_binary_operator, ~c""}
385+
386+
# Containers
387+
assert CF.cursor_context("(foo) ") == {:block_keyword_or_binary_operator, ~c""}
388+
assert CF.cursor_context("[foo] ") == {:block_keyword_or_binary_operator, ~c""}
389+
assert CF.cursor_context("{foo} ") == {:block_keyword_or_binary_operator, ~c""}
390+
assert CF.cursor_context("<<foo>> ") == {:block_keyword_or_binary_operator, ~c""}
391+
392+
# False positives
393+
assert CF.cursor_context("foo ~>> ") == {:operator_call, ~c"~>>"}
394+
assert CF.cursor_context("foo >>> ") == {:operator_call, ~c">>>"}
395+
end
396+
397+
test "keyword from keyword or binary operator" do
398+
# Literals
399+
assert CF.cursor_context("Foo.Bar do") == {:block_keyword_or_binary_operator, ~c"do"}
400+
assert CF.cursor_context("Foo.Bar d") == {:block_keyword_or_binary_operator, ~c"d"}
401+
assert CF.cursor_context("Foo d") == {:block_keyword_or_binary_operator, ~c"d"}
402+
assert CF.cursor_context(":foo d") == {:block_keyword_or_binary_operator, ~c"d"}
403+
assert CF.cursor_context("123 d") == {:block_keyword_or_binary_operator, ~c"d"}
404+
assert CF.cursor_context("nil d") == {:block_keyword_or_binary_operator, ~c"d"}
405+
assert CF.cursor_context("true d") == {:block_keyword_or_binary_operator, ~c"d"}
406+
assert CF.cursor_context("false d") == {:block_keyword_or_binary_operator, ~c"d"}
407+
assert CF.cursor_context("\"foo\" d") == {:block_keyword_or_binary_operator, ~c"d"}
408+
assert CF.cursor_context("'foo' d") == {:block_keyword_or_binary_operator, ~c"d"}
409+
410+
# Containers
411+
assert CF.cursor_context("(foo) d") == {:block_keyword_or_binary_operator, ~c"d"}
412+
assert CF.cursor_context("[foo] d") == {:block_keyword_or_binary_operator, ~c"d"}
413+
assert CF.cursor_context("{foo} d") == {:block_keyword_or_binary_operator, ~c"d"}
414+
assert CF.cursor_context("<<foo>> d") == {:block_keyword_or_binary_operator, ~c"d"}
415+
416+
# False positives
417+
assert CF.cursor_context("foo ~>> d") == {:local_or_var, ~c"d"}
418+
assert CF.cursor_context("foo >>> d") == {:local_or_var, ~c"d"}
419+
end
420+
421+
test "operator from keyword or binary operator" do
422+
# Literals
423+
assert CF.cursor_context("Foo.Bar +") == {:operator, ~c"+"}
424+
assert CF.cursor_context("Foo +") == {:operator, ~c"+"}
425+
assert CF.cursor_context(":foo +") == {:operator, ~c"+"}
426+
assert CF.cursor_context("123 +") == {:operator, ~c"+"}
427+
assert CF.cursor_context("nil +") == {:operator, ~c"+"}
428+
assert CF.cursor_context("true +") == {:operator, ~c"+"}
429+
assert CF.cursor_context("false +") == {:operator, ~c"+"}
430+
assert CF.cursor_context("\"foo\" +") == {:operator, ~c"+"}
431+
assert CF.cursor_context("'foo' +") == {:operator, ~c"+"}
432+
433+
# Containers
434+
assert CF.cursor_context("(foo) +") == {:operator, ~c"+"}
435+
assert CF.cursor_context("[foo] +") == {:operator, ~c"+"}
436+
assert CF.cursor_context("{foo} +") == {:operator, ~c"+"}
437+
assert CF.cursor_context("<<foo>> +") == {:operator, ~c"+"}
438+
439+
# False positives
440+
assert CF.cursor_context("foo ~>> +") == {:operator, ~c"+"}
441+
assert CF.cursor_context("foo >>> +") == {:operator, ~c"+"}
442+
end
443+
371444
test "none" do
372445
# Punctuation
373446
assert CF.cursor_context(")") == :none
@@ -399,8 +472,6 @@ defmodule CodeFragmentTest do
399472
assert CF.cursor_context("HelloWór") == :none
400473
assert CF.cursor_context("@Hello") == :none
401474
assert CF.cursor_context("Hello(") == :none
402-
assert CF.cursor_context("Hello ") == :none
403-
assert CF.cursor_context("hello.World") == {:alias, {:local_or_var, ~c"hello"}, ~c"World"}
404475

405476
# Identifier
406477
assert CF.cursor_context("foo@bar") == :none

Diff for: lib/iex/lib/iex/autocomplete.ex

+26
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,23 @@ defmodule IEx.Autocomplete do
2222
%{kind: :variable, name: "utf32"}
2323
]
2424

25+
block_keywords =
26+
for block_keyword <- ~w(do end after catch else rescue) do
27+
%{kind: :block_keyword, name: block_keyword}
28+
end
29+
30+
binary_operators =
31+
for operator <-
32+
["**", "*", "/", "+", "-", "++", "--", "+++", "---", "..", "<>"] ++
33+
["in", "not in", "|>", "<<<", ">>>", "<<~", "~>>", "<~", "~>", "<~>"] ++
34+
["<", ">", "<=", ">=", "==", "!=", "=~", "===", "!=="] ++
35+
["&&", "&&&", "and", "||", "|||", "or"] ++
36+
["=", "=>", "|", "::", "when", "<-", "\\\\"] do
37+
%{kind: :export, name: operator, arity: 2}
38+
end
39+
40+
@block_keyword_or_binary_operator block_keywords ++ Enum.sort(binary_operators)
41+
2542
@alias_only_atoms ~w(alias import require)a
2643
@alias_only_charlists ~w(alias import require)c
2744

@@ -63,6 +80,9 @@ defmodule IEx.Autocomplete do
6380
{:unquoted_atom, unquoted_atom} ->
6481
expand_erlang_modules(List.to_string(unquoted_atom))
6582

83+
{:block_keyword_or_binary_operator, hint} ->
84+
filter_and_format_expansion(@block_keyword_or_binary_operator, List.to_string(hint))
85+
6686
expansion when helper == ?b ->
6787
expand_typespecs(expansion, shell, &get_module_callbacks/1)
6888

@@ -519,6 +539,12 @@ defmodule IEx.Autocomplete do
519539

520540
## Formatting
521541

542+
defp filter_and_format_expansion(results, hint) do
543+
results
544+
|> Enum.filter(&String.starts_with?(&1.name, hint))
545+
|> format_expansion(hint)
546+
end
547+
522548
defp format_expansion([], _) do
523549
no()
524550
end

Diff for: lib/iex/test/iex/autocomplete_test.exs

+15
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,21 @@ defmodule IEx.AutocompleteTest do
184184
assert expand(~c"IEx.Xyz") == {:no, ~c"", []}
185185
end
186186

187+
test "block keywords" do
188+
assert expand(~c"if true do") == {:yes, ~c"", [~c"do"]}
189+
assert expand(~c"if true a") == {:yes, ~c"", [~c"after", ~c"and/2"]}
190+
assert expand(~c"if true d") == {:yes, ~c"o", []}
191+
assert expand(~c"if true e") == {:yes, ~c"", [~c"end", ~c"else"]}
192+
end
193+
194+
test "block keywords or operators" do
195+
{:yes, ~c"", hints} = expand(~c"if true ")
196+
assert ~c"do" in hints
197+
assert ~c"else" in hints
198+
assert ~c"+/2" in hints
199+
assert ~c"and/2" in hints
200+
end
201+
187202
test "function completion" do
188203
assert expand(~c"System.ve") == {:yes, ~c"rsion", []}
189204
assert expand(~c":ets.fun2") == {:yes, ~c"ms", []}

0 commit comments

Comments
 (0)