Skip to content

Commit 10b4d76

Browse files
committed
Handle new cursor context from Elixir 1.19+
1 parent 6b74cfc commit 10b4d76

5 files changed

Lines changed: 133 additions & 25 deletions

File tree

lib/livebook/intellisense/elixir.ex

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,22 @@ defmodule Livebook.Intellisense.Elixir do
228228
}
229229
end
230230

231+
defp format_completion_item(%{kind: :keyword, name: name}),
232+
do: %{
233+
label: Atom.to_string(name),
234+
kind: :keyword,
235+
documentation: "(keyword)",
236+
insert_text: Atom.to_string(name)
237+
}
238+
239+
defp format_completion_item(%{kind: :binary_operator, name: name, arity: arity}),
240+
do: %{
241+
label: "#{name}/#{arity}",
242+
kind: :binary_operator,
243+
documentation: "(binary operator)",
244+
insert_text: Atom.to_string(name)
245+
}
246+
231247
defp keyword_macro?(name) do
232248
def? = name |> Atom.to_string() |> String.starts_with?("def")
233249

@@ -306,7 +322,8 @@ defmodule Livebook.Intellisense.Elixir do
306322
:interface,
307323
:function,
308324
:type,
309-
:bitstring_option
325+
:bitstring_option,
326+
:binary_operator
310327
]
311328

312329
defp completion_item_priority(%{kind: :struct} = completion_item) do
@@ -419,6 +436,8 @@ defmodule Livebook.Intellisense.Elixir do
419436
])
420437
end
421438

439+
defp format_details_item(%{kind: :keyword, name: name}), do: code(name)
440+
422441
defp get_definition_location(%{kind: :module, module: module}, context) do
423442
get_definition_location(module, context, {:module, module})
424443
end

lib/livebook/intellisense/elixir/identifier_matcher.ex

Lines changed: 75 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ defmodule Livebook.Intellisense.Elixir.IdentifierMatcher do
7272
name: name(),
7373
arity: integer()
7474
}
75+
| %{kind: :keyword, name: name()}
76+
| %{kind: :binary_operator, name: name(), arity: arity()}
7577

7678
@type name :: atom()
7779
@type display_name :: String.t()
@@ -99,6 +101,26 @@ defmodule Livebook.Intellisense.Elixir.IdentifierMatcher do
99101
@alias_only_atoms ~w(alias import require)a
100102
@alias_only_charlists ~w(alias import require)c
101103

104+
@block_keywords ~w(do end after catch else rescue)
105+
106+
@kernel_operators ["**", "*", "/", "+", "-", "++", "--", "..", "<>"] ++
107+
["in", "|>", "<", ">", "<=", ">=", "==", "!=", "=~", "===", "!=="] ++
108+
["&&", "and", "||", "or", "="]
109+
110+
@bitwise_operators ["<<<", ">>>", "&&&", "|||"]
111+
112+
@special_forms_operators ["::", "="]
113+
114+
@binary_operators ["+++", "---", "not in", "<<~", "~>>", "<~", "~>", "<~>"] ++
115+
["=>", "|", "when", "<-", "\\\\"]
116+
117+
@binary_operators_docs [
118+
{Kernel, @kernel_operators},
119+
{Kernel.SpecialForms, @special_forms_operators},
120+
{Bitwise, @bitwise_operators},
121+
{nil, @binary_operators}
122+
]
123+
102124
@doc """
103125
Clears all loaded entries stored for node.
104126
"""
@@ -247,6 +269,9 @@ defmodule Livebook.Intellisense.Elixir.IdentifierMatcher do
247269
{:struct, struct} ->
248270
match_struct(List.to_string(struct), ctx)
249271

272+
{:block_keyword_or_binary_operator, hint} ->
273+
match_block_keyword_or_binary_operator(List.to_string(hint), ctx)
274+
250275
# :none
251276
_ ->
252277
[]
@@ -351,6 +376,35 @@ defmodule Livebook.Intellisense.Elixir.IdentifierMatcher do
351376
do: item
352377
end
353378

379+
defp match_block_keyword_or_binary_operator(hint, ctx) do
380+
block_keywords =
381+
for value <- match_values_from_hint(hint, @block_keywords, ctx) do
382+
%{kind: :keyword, name: value}
383+
end
384+
385+
binary_operators =
386+
for {mod, values} <- @binary_operators_docs,
387+
name <- match_values_from_hint(hint, values, ctx) do
388+
if mod do
389+
match_module_function(mod, hint, ctx)
390+
else
391+
%{kind: :binary_operator, name: name, arity: 2}
392+
end
393+
end
394+
395+
result = List.flatten(block_keywords ++ binary_operators)
396+
397+
if result != [] do
398+
result
399+
end
400+
end
401+
402+
defp match_values_from_hint(hint, values, ctx) do
403+
for value <- values, ctx.matcher.(value, hint) do
404+
String.to_existing_atom(value)
405+
end
406+
end
407+
354408
defp has_struct?(mod) do
355409
Code.ensure_loaded?(mod) and function_exported?(mod, :__struct__, 1)
356410
end
@@ -690,34 +744,33 @@ defmodule Livebook.Intellisense.Elixir.IdentifierMatcher do
690744
)
691745

692746
Enum.map(matching_funs, fn {name, arity, type} ->
693-
doc_item =
694-
Enum.find(
695-
doc_items,
696-
%{from_default: false, documentation: nil, signatures: [], specs: [], meta: %{}},
697-
fn doc_item ->
698-
doc_item.name == name && doc_item.arity == arity
699-
end
700-
)
701-
702-
%{
703-
kind: :function,
704-
module: mod,
705-
name: name,
706-
arity: arity,
707-
type: type,
708-
display_name: Atom.to_string(name),
709-
from_default: doc_item.from_default,
710-
documentation: doc_item.documentation,
711-
signatures: doc_item.signatures,
712-
specs: doc_item.specs,
713-
meta: doc_item.meta
714-
}
747+
function_with_docs(mod, doc_items, name, arity, type)
715748
end)
716749
else
717750
[]
718751
end
719752
end
720753

754+
defp function_with_docs(mod, doc_items, name, arity, type) do
755+
item = %{
756+
kind: :function,
757+
module: mod,
758+
name: name,
759+
arity: arity,
760+
type: type,
761+
display_name: Atom.to_string(name)
762+
}
763+
764+
doc_item =
765+
if doc_item = Enum.find(doc_items, &(&1.name == name and &1.arity == arity)) do
766+
Map.take(doc_item, [:documentation, :from_default, :meta, :signatures, :specs])
767+
else
768+
%{from_default: false, documentation: nil, signatures: [], specs: [], meta: %{}}
769+
end
770+
771+
Map.merge(item, doc_item)
772+
end
773+
721774
defp exports(mod, node) do
722775
try do
723776
:erpc.call(node, mod, :module_info, [:exports])

mix.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ end
55
defmodule Livebook.MixProject do
66
use Mix.Project
77

8-
@elixir_requirement "~> 1.18"
8+
@elixir_requirement "~> 1.20"
99
@version "0.20.0-dev"
1010
@description "Automate code & data workflows with interactive notebooks"
1111

test/livebook/intellisense/elixir_test.exs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1685,6 +1685,42 @@ defmodule Livebook.Intellisense.ElixirTest do
16851685

16861686
refute Enum.any?(items, &(&1.kind == :function))
16871687
end
1688+
1689+
test "completion for block keywords and binary operators" do
1690+
context = intellisense_context_from_eval(do: nil)
1691+
1692+
assert %{
1693+
items: [
1694+
%{documentation: "(keyword)", insert_text: "do", kind: :keyword, label: "do"}
1695+
]
1696+
} = Intellisense.Elixir.handle_request({:completion, "if true d"}, context, node())
1697+
1698+
assert %{items: items} =
1699+
Intellisense.Elixir.handle_request({:completion, "if 1 "}, context, node())
1700+
1701+
assert %{
1702+
documentation: "(binary operator)",
1703+
insert_text: "\\\\",
1704+
kind: :binary_operator,
1705+
label: "\\\\/2"
1706+
} in items
1707+
1708+
assert %{items: items} =
1709+
Intellisense.Elixir.handle_request({:completion, "if 1 +"}, context, node())
1710+
1711+
assert %{
1712+
documentation: """
1713+
Arithmetic addition operator.
1714+
1715+
```
1716+
left + right
1717+
```\
1718+
""",
1719+
insert_text: "+",
1720+
kind: :function,
1721+
label: "+/2"
1722+
} in items
1723+
end
16881724
end
16891725

16901726
describe "details" do

versions

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# across three versions. So, for example, if Rebar3 is compiled for 26,
44
# then the executable will be good for 26..28. You can find the executable
55
# version in Rebar3 CI files.
6-
elixir="1.19.3"
6+
elixir="1.20.1"
77
otp="28.1.1"
88
rebar3="3.24.0"
99
ubuntu="noble-20251013"

0 commit comments

Comments
 (0)