From 86565f69f0bef966410076b1d76d7bd7b30e994b Mon Sep 17 00:00:00 2001 From: kxlsx Date: Fri, 21 Nov 2025 17:45:48 +0100 Subject: [PATCH 01/26] Erlang Intellisense initial module item completion 1. Added Intellisense completion request handling for module items (syntax akin to: `MODULE:FUNC_PREFIX`). 2. Added skeleton code for other types of identifier completion This implementation relies heavily on functionality inside `Livebook.Intellisense.Elixir`, so a couple of private functions were made public. I would consider moving a lot of generic functionality from the Elixir specific modules into a separate one. --- lib/livebook/intellisense/elixir.ex | 31 +++--- .../intellisense/elixir/identifier_matcher.ex | 4 +- lib/livebook/intellisense/erlang.ex | 27 +++-- .../intellisense/erlang/identifier_matcher.ex | 102 ++++++++++++++++++ lib/livebook/runtime/erl_dist.ex | 1 + test/livebook/intellisense/erlang_test.exs | 1 + 6 files changed, 144 insertions(+), 22 deletions(-) create mode 100644 lib/livebook/intellisense/erlang/identifier_matcher.ex create mode 100644 test/livebook/intellisense/erlang_test.exs diff --git a/lib/livebook/intellisense/elixir.ex b/lib/livebook/intellisense/elixir.ex index f1ee0b1cd04..3898f7b1f18 100644 --- a/lib/livebook/intellisense/elixir.ex +++ b/lib/livebook/intellisense/elixir.ex @@ -49,12 +49,13 @@ defmodule Livebook.Intellisense.Elixir do %{items: items} end + # FIXME: made a lot of stuff public - defp include_in_completion?(%{kind: :module, documentation: :hidden}), do: false - defp include_in_completion?(%{kind: :function, documentation: :hidden}), do: false - defp include_in_completion?(_), do: true + def include_in_completion?(%{kind: :module, documentation: :hidden}), do: false + def include_in_completion?(%{kind: :function, documentation: :hidden}), do: false + def include_in_completion?(_), do: true - defp format_completion_item(%{kind: :variable, name: name}), + def format_completion_item(%{kind: :variable, name: name}), do: %{ label: Atom.to_string(name), kind: :variable, @@ -62,7 +63,7 @@ defmodule Livebook.Intellisense.Elixir do insert_text: Atom.to_string(name) } - defp format_completion_item(%{kind: :map_field, name: name}), + def format_completion_item(%{kind: :map_field, name: name}), do: %{ label: Atom.to_string(name), kind: :field, @@ -70,7 +71,7 @@ defmodule Livebook.Intellisense.Elixir do insert_text: Atom.to_string(name) } - defp format_completion_item(%{kind: :in_map_field, name: name}), + def format_completion_item(%{kind: :in_map_field, name: name}), do: %{ label: Atom.to_string(name), kind: :field, @@ -78,7 +79,7 @@ defmodule Livebook.Intellisense.Elixir do insert_text: "#{name}: " } - defp format_completion_item(%{ + def format_completion_item(%{ kind: :in_struct_field, struct: struct, name: name, @@ -102,7 +103,7 @@ defmodule Livebook.Intellisense.Elixir do insert_text: "#{name}: " } - defp format_completion_item(%{ + def format_completion_item(%{ kind: :module, module: module, display_name: display_name, @@ -133,7 +134,7 @@ defmodule Livebook.Intellisense.Elixir do } end - defp format_completion_item(%{ + def format_completion_item(%{ kind: :function, module: module, name: name, @@ -174,7 +175,7 @@ defmodule Livebook.Intellisense.Elixir do end } - defp format_completion_item(%{ + def format_completion_item(%{ kind: :type, name: name, arity: arity, @@ -196,7 +197,7 @@ defmodule Livebook.Intellisense.Elixir do end } - defp format_completion_item(%{ + def format_completion_item(%{ kind: :module_attribute, name: name, documentation: documentation @@ -212,7 +213,7 @@ defmodule Livebook.Intellisense.Elixir do insert_text: Atom.to_string(name) } - defp format_completion_item(%{kind: :bitstring_modifier, name: name, arity: arity}) do + def format_completion_item(%{kind: :bitstring_modifier, name: name, arity: arity}) do insert_text = if arity == 0 do Atom.to_string(name) @@ -260,7 +261,7 @@ defmodule Livebook.Intellisense.Elixir do name in [:__ENV__, :__MODULE__, :__DIR__, :__STACKTRACE__, :__CALLER__] end - defp extra_completion_items(hint) do + def extra_completion_items(hint) do items = [ %{ label: "true", @@ -309,7 +310,7 @@ defmodule Livebook.Intellisense.Elixir do :bitstring_option ] - defp completion_item_priority(%{kind: :struct} = completion_item) do + def completion_item_priority(%{kind: :struct} = completion_item) do if completion_item.documentation =~ "(exception)" do {length(@ordered_kinds), completion_item.label} else @@ -317,7 +318,7 @@ defmodule Livebook.Intellisense.Elixir do end end - defp completion_item_priority(completion_item) do + def completion_item_priority(completion_item) do {completion_item_kind_priority(completion_item.kind), completion_item.label} end diff --git a/lib/livebook/intellisense/elixir/identifier_matcher.ex b/lib/livebook/intellisense/elixir/identifier_matcher.ex index 592d35ad3c7..9f616d7a54e 100644 --- a/lib/livebook/intellisense/elixir/identifier_matcher.ex +++ b/lib/livebook/intellisense/elixir/identifier_matcher.ex @@ -99,6 +99,7 @@ defmodule Livebook.Intellisense.Elixir.IdentifierMatcher do @alias_only_atoms ~w(alias import require)a @alias_only_charlists ~w(alias import require)c + @spec clear_all_loaded(any()) :: boolean() @doc """ Clears all loaded entries stored for node. """ @@ -671,7 +672,8 @@ defmodule Livebook.Intellisense.Elixir.IdentifierMatcher do do: module end - defp match_module_function(mod, hint, ctx, funs \\ nil) do + # FIXME: THIS IS PUBLIC + def match_module_function(mod, hint, ctx, funs \\ nil) do if ensure_loaded?(mod, ctx.node) do funs = funs || exports(mod, ctx.node) diff --git a/lib/livebook/intellisense/erlang.ex b/lib/livebook/intellisense/erlang.ex index 849b0ff7ec0..cbb0381167c 100644 --- a/lib/livebook/intellisense/erlang.ex +++ b/lib/livebook/intellisense/erlang.ex @@ -1,4 +1,5 @@ defmodule Livebook.Intellisense.Erlang do + alias Livebook.Intellisense @behaviour Intellisense @@ -9,8 +10,8 @@ defmodule Livebook.Intellisense.Erlang do nil end - def handle_request({:completion, hint}, context, _node) do - handle_completion(hint, context) + def handle_request({:completion, hint}, context, node) do + handle_completion(hint, context, node) end def handle_request({:details, line, column}, context, _node) do @@ -21,18 +22,32 @@ defmodule Livebook.Intellisense.Erlang do handle_signature(hint, context) end - defp handle_completion(_hint, _context) do + defp handle_completion(hint, context, node) do # TODO: implement. See t:Livebook.Runtime.completion_response/0 for return type. - nil + IO.write("completion:") + + items = Intellisense.Erlang.IdentifierMatcher.completion_identifiers(hint, context, node) + |> Enum.filter(&Intellisense.Elixir.include_in_completion?/1) + |> Enum.map(&Intellisense.Elixir.format_completion_item/1) + |> Enum.concat(Intellisense.Elixir.extra_completion_items(hint)) + |> Enum.sort_by(&Intellisense.Elixir.completion_item_priority/1) + + IO.inspect(items) + + %{items: items} end - defp handle_details(_line, _column, _context) do + defp handle_details(line, _column, _context) do # TODO: implement. See t:Livebook.Runtime.details_response/0 for return type. + IO.write("details:") + IO.inspect(line) nil end - defp handle_signature(_hint, _context) do + defp handle_signature(hint, _context) do # TODO: implement. See t:Livebook.Runtime.signature_response/0 for return type. + IO.write("signature:") + IO.inspect(hint) nil end end diff --git a/lib/livebook/intellisense/erlang/identifier_matcher.ex b/lib/livebook/intellisense/erlang/identifier_matcher.ex new file mode 100644 index 00000000000..2fc0d29a6ba --- /dev/null +++ b/lib/livebook/intellisense/erlang/identifier_matcher.ex @@ -0,0 +1,102 @@ +defmodule Livebook.Intellisense.Erlang.IdentifierMatcher do + alias Livebook.Intellisense + + @type identifier_item :: + %{ + kind: :variable, + name: name() + } + | %{ + kind: :module, + module: module(), + display_name: display_name(), + } + | %{ + kind: :function, + module: module(), + name: name(), + arity: arity(), + display_name: display_name(), + } + | %{ + kind: :keyword, + name: name(), + } + | %{ + kind: :bitstring_modifier, + name: name(), + arity: integer() + } + + @type name :: atom() + @type display_name :: String.t() + + @prefix_matcher &String.starts_with?/2 + + def completion_identifiers(hint, intellisense_context, node) do + context = cursor_context(hint) + + ctx = %{ + fragment: hint, + intellisense_context: intellisense_context, + matcher: @prefix_matcher, + type: :completion, + node: node, + } + + context_to_matches(context, ctx) + end + + defp context_to_matches(context, ctx) do + case context do + {:mod_func, mod, func} -> + Intellisense.Elixir.IdentifierMatcher.match_module_function( + mod, Atom.to_string(func), ctx + ) + # TODO: all this: + {:macro, macro} -> + [] + {:pre_directive, directive} -> + [] + {:atom, atom} -> + [] + {:var, var} -> + [] + # TODO: bitstrings, need to be parsed! + :expr -> + [] + + # :none + _ -> + [] + end + end + + defp cursor_context(hint) do + case :erl_scan.string(String.to_charlist(hint)) do + {:error, _, _} -> + :none + {:ok, tokens, _} -> + match_tokens_to_context(Enum.reverse(tokens)) + end + end + + defp match_tokens_to_context([{:atom, _, func}, {:":", _}, {:atom, _, mod} | _]), + do: {:mod_func, mod, func} + defp match_tokens_to_context([{:":", _}, {:atom, _, mod} | _]), + do: {:mod_func, mod, :""} + defp match_tokens_to_context([{:atom, _, macro}, {:"?", _} | _]), + do: {:macro, macro} + defp match_tokens_to_context([{:var, _, macro}, {:"?", _} | _]), + do: {:macro, macro} + defp match_tokens_to_context([{:atom, _, directive}, {:"-", _} | _]), + do: {:pre_directive, directive} + defp match_tokens_to_context([{:atom, _, atom} | _]), + do: {:atom, atom} + defp match_tokens_to_context([{:var, _, var} | _]), + do: {:var, var} + defp match_tokens_to_context([]), + do: :none + defp match_tokens_to_context(_), + do: :expr +end diff --git a/lib/livebook/runtime/erl_dist.ex b/lib/livebook/runtime/erl_dist.ex index 3e08bdfc518..11feafac8fb 100644 --- a/lib/livebook/runtime/erl_dist.ex +++ b/lib/livebook/runtime/erl_dist.ex @@ -36,6 +36,7 @@ defmodule Livebook.Runtime.ErlDist do Livebook.Intellisense.Elixir.IdentifierMatcher, Livebook.Intellisense.Elixir.SignatureMatcher, Livebook.Intellisense.Erlang, + Livebook.Intellisense.Erlang.IdentifierMatcher, Livebook.Runtime.ErlDist, Livebook.Runtime.ErlDist.NodeManager, Livebook.Runtime.ErlDist.RuntimeServer, diff --git a/test/livebook/intellisense/erlang_test.exs b/test/livebook/intellisense/erlang_test.exs new file mode 100644 index 00000000000..1a73ae6336b --- /dev/null +++ b/test/livebook/intellisense/erlang_test.exs @@ -0,0 +1 @@ +#TODO: this: From 6c8bcbe4bd85adb64c75fe64c6a5e6c82ddf5ad3 Mon Sep 17 00:00:00 2001 From: kxlsx Date: Sat, 22 Nov 2025 16:34:28 +0100 Subject: [PATCH 02/26] Added Variable completion. Variable completion just utilizes the current elixir infrastructure, and there's no ambiguity, as erlang variables always start with a capitalized letter. Also needed to use elixir to erlang variable name conversion from `lib/livebook/runtime/evaluator.ex` --- lib/livebook/intellisense/elixir/identifier_matcher.ex | 3 ++- lib/livebook/intellisense/erlang.ex | 2 -- lib/livebook/intellisense/erlang/identifier_matcher.ex | 10 ++++++---- lib/livebook/runtime/evaluator.ex | 5 +++-- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/lib/livebook/intellisense/elixir/identifier_matcher.ex b/lib/livebook/intellisense/elixir/identifier_matcher.ex index 9f616d7a54e..a091ca80dbe 100644 --- a/lib/livebook/intellisense/elixir/identifier_matcher.ex +++ b/lib/livebook/intellisense/elixir/identifier_matcher.ex @@ -495,7 +495,8 @@ defmodule Livebook.Intellisense.Elixir.IdentifierMatcher do imports ++ special_forms end - defp match_variable(hint, ctx) do + # FIXME: THIS IS PUBLIC + def match_variable(hint, ctx) do for {var, nil} <- Macro.Env.vars(ctx.intellisense_context.env), name = Atom.to_string(var), ctx.matcher.(name, hint), diff --git a/lib/livebook/intellisense/erlang.ex b/lib/livebook/intellisense/erlang.ex index cbb0381167c..dd7f37e5b71 100644 --- a/lib/livebook/intellisense/erlang.ex +++ b/lib/livebook/intellisense/erlang.ex @@ -32,8 +32,6 @@ defmodule Livebook.Intellisense.Erlang do |> Enum.concat(Intellisense.Elixir.extra_completion_items(hint)) |> Enum.sort_by(&Intellisense.Elixir.completion_item_priority/1) - IO.inspect(items) - %{items: items} end diff --git a/lib/livebook/intellisense/erlang/identifier_matcher.ex b/lib/livebook/intellisense/erlang/identifier_matcher.ex index 2fc0d29a6ba..a4f25114388 100644 --- a/lib/livebook/intellisense/erlang/identifier_matcher.ex +++ b/lib/livebook/intellisense/erlang/identifier_matcher.ex @@ -50,9 +50,7 @@ defmodule Livebook.Intellisense.Erlang.IdentifierMatcher do defp context_to_matches(context, ctx) do case context do {:mod_func, mod, func} -> - Intellisense.Elixir.IdentifierMatcher.match_module_function( - mod, Atom.to_string(func), ctx - ) + Intellisense.Elixir.IdentifierMatcher.match_module_function(mod, Atom.to_string(func), ctx) # TODO: all this: {:macro, macro} -> [] @@ -61,7 +59,11 @@ defmodule Livebook.Intellisense.Erlang.IdentifierMatcher do {:atom, atom} -> [] {:var, var} -> - [] + var + |> Livebook.Runtime.Evaluator.erlang_to_elixir_var + |> to_string + |> Intellisense.Elixir.IdentifierMatcher.match_variable(ctx) + |> Enum.map(&%{&1 | name: Livebook.Runtime.Evaluator.elixir_to_erlang_var(&1[:name])}) # TODO: bitstrings, need to be parsed! :expr -> [] diff --git a/lib/livebook/runtime/evaluator.ex b/lib/livebook/runtime/evaluator.ex index ad8b4ec4f33..c90ff662574 100644 --- a/lib/livebook/runtime/evaluator.ex +++ b/lib/livebook/runtime/evaluator.ex @@ -885,14 +885,15 @@ defmodule Livebook.Runtime.Evaluator do end end - defp elixir_to_erlang_var(name) do + #FIXME: THIS IS PUBLIC NOW + def elixir_to_erlang_var(name) do name |> :erlang.atom_to_binary() |> toggle_var_case() |> :erlang.binary_to_atom() end - defp erlang_to_elixir_var(name) do + def erlang_to_elixir_var(name) do name |> :erlang.atom_to_binary() |> toggle_var_case() From 5572d0dd0d5d9c7121bff3a214b63823897ab4b4 Mon Sep 17 00:00:00 2001 From: krotka Date: Thu, 4 Dec 2025 01:36:17 +0100 Subject: [PATCH 03/26] Case statement instead of signature pattern match --- .../intellisense/erlang/identifier_matcher.ex | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/lib/livebook/intellisense/erlang/identifier_matcher.ex b/lib/livebook/intellisense/erlang/identifier_matcher.ex index a4f25114388..41b72669f6f 100644 --- a/lib/livebook/intellisense/erlang/identifier_matcher.ex +++ b/lib/livebook/intellisense/erlang/identifier_matcher.ex @@ -83,22 +83,22 @@ defmodule Livebook.Intellisense.Erlang.IdentifierMatcher do end end - defp match_tokens_to_context([{:atom, _, func}, {:":", _}, {:atom, _, mod} | _]), - do: {:mod_func, mod, func} - defp match_tokens_to_context([{:":", _}, {:atom, _, mod} | _]), - do: {:mod_func, mod, :""} - defp match_tokens_to_context([{:atom, _, macro}, {:"?", _} | _]), - do: {:macro, macro} - defp match_tokens_to_context([{:var, _, macro}, {:"?", _} | _]), - do: {:macro, macro} - defp match_tokens_to_context([{:atom, _, directive}, {:"-", _} | _]), - do: {:pre_directive, directive} - defp match_tokens_to_context([{:atom, _, atom} | _]), - do: {:atom, atom} - defp match_tokens_to_context([{:var, _, var} | _]), - do: {:var, var} - defp match_tokens_to_context([]), - do: :none - defp match_tokens_to_context(_), - do: :expr + defp match_tokens_to_context(tokens) do + case tokens do + [{:atom, _, func}, {:":", _}, {:atom, _, mod} | _] -> {:mod_func, mod, func} + [ {:":", _}, {:atom, _, mod} | _] -> {:mod_func, mod, :""} + + [{:atom, _, macro}, {:"?", _} | _] -> {:macro, macro} + [{:var, _, macro}, {:"?", _} | _] -> {:macro, macro} + + [{:atom, _, directive}, {:"-", _} | _] -> {:pre_directive, directive} + + [{:atom, _, atom} | _] -> {:atom, atom} + + [{:var, _, var} | _] -> {:var, var} + + [] -> :none + _ -> :expr + end + end end From 8ded1c5ca2d4f6d18616ae9d6b3998fe2ae9c0bc Mon Sep 17 00:00:00 2001 From: krotka Date: Thu, 4 Dec 2025 02:31:07 +0100 Subject: [PATCH 04/26] Merge exported functions into one pipeline A lot of these exports were always used in the same pipeline anyways, so I made them private and exported them as said pipeline --- lib/livebook/intellisense/elixir.ex | 40 ++++++++++++++++------------- lib/livebook/intellisense/erlang.ex | 9 ++----- 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/lib/livebook/intellisense/elixir.ex b/lib/livebook/intellisense/elixir.ex index 3898f7b1f18..d4ba33be6bf 100644 --- a/lib/livebook/intellisense/elixir.ex +++ b/lib/livebook/intellisense/elixir.ex @@ -40,22 +40,26 @@ defmodule Livebook.Intellisense.Elixir do end defp handle_completion(hint, context, node) do + Intellisense.Elixir.IdentifierMatcher.completion_identifiers(hint, context, node) + |> format_completion_identifiers(extra_completion_items(hint)) + end + + def format_completion_identifiers(completions, extra \\ []) do items = - Intellisense.Elixir.IdentifierMatcher.completion_identifiers(hint, context, node) + completions |> Enum.filter(&include_in_completion?/1) |> Enum.map(&format_completion_item/1) - |> Enum.concat(extra_completion_items(hint)) + |> Enum.concat(extra) |> Enum.sort_by(&completion_item_priority/1) %{items: items} end - # FIXME: made a lot of stuff public - def include_in_completion?(%{kind: :module, documentation: :hidden}), do: false - def include_in_completion?(%{kind: :function, documentation: :hidden}), do: false - def include_in_completion?(_), do: true + defp include_in_completion?(%{kind: :module, documentation: :hidden}), do: false + defp include_in_completion?(%{kind: :function, documentation: :hidden}), do: false + defp include_in_completion?(_), do: true - def format_completion_item(%{kind: :variable, name: name}), + defp format_completion_item(%{kind: :variable, name: name}), do: %{ label: Atom.to_string(name), kind: :variable, @@ -63,7 +67,7 @@ defmodule Livebook.Intellisense.Elixir do insert_text: Atom.to_string(name) } - def format_completion_item(%{kind: :map_field, name: name}), + defp format_completion_item(%{kind: :map_field, name: name}), do: %{ label: Atom.to_string(name), kind: :field, @@ -71,7 +75,7 @@ defmodule Livebook.Intellisense.Elixir do insert_text: Atom.to_string(name) } - def format_completion_item(%{kind: :in_map_field, name: name}), + defp format_completion_item(%{kind: :in_map_field, name: name}), do: %{ label: Atom.to_string(name), kind: :field, @@ -79,7 +83,7 @@ defmodule Livebook.Intellisense.Elixir do insert_text: "#{name}: " } - def format_completion_item(%{ + defp format_completion_item(%{ kind: :in_struct_field, struct: struct, name: name, @@ -103,7 +107,7 @@ defmodule Livebook.Intellisense.Elixir do insert_text: "#{name}: " } - def format_completion_item(%{ + defp format_completion_item(%{ kind: :module, module: module, display_name: display_name, @@ -134,7 +138,7 @@ defmodule Livebook.Intellisense.Elixir do } end - def format_completion_item(%{ + defp format_completion_item(%{ kind: :function, module: module, name: name, @@ -175,7 +179,7 @@ defmodule Livebook.Intellisense.Elixir do end } - def format_completion_item(%{ + defp format_completion_item(%{ kind: :type, name: name, arity: arity, @@ -197,7 +201,7 @@ defmodule Livebook.Intellisense.Elixir do end } - def format_completion_item(%{ + defp format_completion_item(%{ kind: :module_attribute, name: name, documentation: documentation @@ -213,7 +217,7 @@ defmodule Livebook.Intellisense.Elixir do insert_text: Atom.to_string(name) } - def format_completion_item(%{kind: :bitstring_modifier, name: name, arity: arity}) do + defp format_completion_item(%{kind: :bitstring_modifier, name: name, arity: arity}) do insert_text = if arity == 0 do Atom.to_string(name) @@ -261,7 +265,7 @@ defmodule Livebook.Intellisense.Elixir do name in [:__ENV__, :__MODULE__, :__DIR__, :__STACKTRACE__, :__CALLER__] end - def extra_completion_items(hint) do + defp extra_completion_items(hint) do items = [ %{ label: "true", @@ -310,7 +314,7 @@ defmodule Livebook.Intellisense.Elixir do :bitstring_option ] - def completion_item_priority(%{kind: :struct} = completion_item) do + defp completion_item_priority(%{kind: :struct} = completion_item) do if completion_item.documentation =~ "(exception)" do {length(@ordered_kinds), completion_item.label} else @@ -318,7 +322,7 @@ defmodule Livebook.Intellisense.Elixir do end end - def completion_item_priority(completion_item) do + defp completion_item_priority(completion_item) do {completion_item_kind_priority(completion_item.kind), completion_item.label} end diff --git a/lib/livebook/intellisense/erlang.ex b/lib/livebook/intellisense/erlang.ex index dd7f37e5b71..03431c7494a 100644 --- a/lib/livebook/intellisense/erlang.ex +++ b/lib/livebook/intellisense/erlang.ex @@ -26,13 +26,8 @@ defmodule Livebook.Intellisense.Erlang do # TODO: implement. See t:Livebook.Runtime.completion_response/0 for return type. IO.write("completion:") - items = Intellisense.Erlang.IdentifierMatcher.completion_identifiers(hint, context, node) - |> Enum.filter(&Intellisense.Elixir.include_in_completion?/1) - |> Enum.map(&Intellisense.Elixir.format_completion_item/1) - |> Enum.concat(Intellisense.Elixir.extra_completion_items(hint)) - |> Enum.sort_by(&Intellisense.Elixir.completion_item_priority/1) - - %{items: items} + Intellisense.Erlang.IdentifierMatcher.completion_identifiers(hint, context, node) + |> Intellisense.Elixir.format_completion_identifiers() end defp handle_details(line, _column, _context) do From 7c1dfd3c089eed93c4cc37e902b9b66eda40ecd8 Mon Sep 17 00:00:00 2001 From: Filip Date: Tue, 9 Dec 2025 18:22:34 +0100 Subject: [PATCH 05/26] first working version of docs for modules and functions --- lib/livebook/intellisense/elixir.ex | 33 +++++----- .../intellisense/elixir/identifier_matcher.ex | 2 +- lib/livebook/intellisense/erlang.ex | 24 ++++--- .../intellisense/erlang/identifier_matcher.ex | 62 +++++++++++++++++++ 4 files changed, 96 insertions(+), 25 deletions(-) diff --git a/lib/livebook/intellisense/elixir.ex b/lib/livebook/intellisense/elixir.ex index d4ba33be6bf..64dacd0bfad 100644 --- a/lib/livebook/intellisense/elixir.ex +++ b/lib/livebook/intellisense/elixir.ex @@ -343,22 +343,21 @@ defmodule Livebook.Intellisense.Elixir do contents = Enum.map(matches, &format_details_item/1) definition = get_definition_location(hd(matches), context) - %{range: range, contents: contents, definition: definition} end end - defp include_in_details?(%{kind: :function, from_default: true}), do: false - defp include_in_details?(%{kind: :bitstring_modifier}), do: false - defp include_in_details?(_), do: true + def include_in_details?(%{kind: :function, from_default: true}), do: false + def include_in_details?(%{kind: :bitstring_modifier}), do: false + def include_in_details?(_), do: true - defp format_details_item(%{kind: :variable, name: name}), do: code(name) + def format_details_item(%{kind: :variable, name: name}), do: code(name) - defp format_details_item(%{kind: :map_field, name: name}), do: code(name) + def format_details_item(%{kind: :map_field, name: name}), do: code(name) - defp format_details_item(%{kind: :in_map_field, name: name}), do: code(name) + def format_details_item(%{kind: :in_map_field, name: name}), do: code(name) - defp format_details_item(%{kind: :in_struct_field, name: name, default: default}) do + def format_details_item(%{kind: :in_struct_field, name: name, default: default}) do join_with_divider([ code(name), """ @@ -371,7 +370,7 @@ defmodule Livebook.Intellisense.Elixir do ]) end - defp format_details_item(%{kind: :module, module: module, documentation: documentation}) do + def format_details_item(%{kind: :module, module: module, documentation: documentation}) do join_with_divider([ code(inspect(module)), format_docs_link(module), @@ -379,7 +378,7 @@ defmodule Livebook.Intellisense.Elixir do ]) end - defp format_details_item(%{ + def format_details_item(%{ kind: :function, module: module, name: name, @@ -401,7 +400,7 @@ defmodule Livebook.Intellisense.Elixir do ]) end - defp format_details_item(%{ + def format_details_item(%{ kind: :type, module: module, name: name, @@ -417,31 +416,31 @@ defmodule Livebook.Intellisense.Elixir do ]) end - defp format_details_item(%{kind: :module_attribute, name: name, documentation: documentation}) do + def format_details_item(%{kind: :module_attribute, name: name, documentation: documentation}) do join_with_divider([ code("@#{name}"), Intellisense.Elixir.Docs.format_documentation(documentation, :all) ]) end - defp get_definition_location(%{kind: :module, module: module}, context) do + def get_definition_location(%{kind: :module, module: module}, context) do get_definition_location(module, context, {:module, module}) end - defp get_definition_location( + def get_definition_location( %{kind: :function, module: module, name: name, arity: arity}, context ) do get_definition_location(module, context, {:function, name, arity}) end - defp get_definition_location(%{kind: :type, module: module, name: name, arity: arity}, context) do + def get_definition_location(%{kind: :type, module: module, name: name, arity: arity}, context) do get_definition_location(module, context, {:type, name, arity}) end - defp get_definition_location(_idenfitier, _context), do: nil + def get_definition_location(_idenfitier, _context), do: nil - defp get_definition_location(module, context, identifier) do + def get_definition_location(module, context, identifier) do if context.ebin_path do path = Path.join(context.ebin_path, "#{module}.beam") diff --git a/lib/livebook/intellisense/elixir/identifier_matcher.ex b/lib/livebook/intellisense/elixir/identifier_matcher.ex index a091ca80dbe..913b8e2e332 100644 --- a/lib/livebook/intellisense/elixir/identifier_matcher.ex +++ b/lib/livebook/intellisense/elixir/identifier_matcher.ex @@ -519,7 +519,7 @@ defmodule Livebook.Intellisense.Elixir.IdentifierMatcher do do: %{item | display_name: "~" <> sigil_name} end - defp match_erlang_module(hint, ctx) do + def match_erlang_module(hint, ctx) do for mod <- get_matching_modules(hint, ctx), usable_as_unquoted_module?(mod), name = ":" <> Atom.to_string(mod), diff --git a/lib/livebook/intellisense/erlang.ex b/lib/livebook/intellisense/erlang.ex index 03431c7494a..f70428a59ca 100644 --- a/lib/livebook/intellisense/erlang.ex +++ b/lib/livebook/intellisense/erlang.ex @@ -14,8 +14,8 @@ defmodule Livebook.Intellisense.Erlang do handle_completion(hint, context, node) end - def handle_request({:details, line, column}, context, _node) do - handle_details(line, column, context) + def handle_request({:details, line, column}, context, node) do + handle_details(line, column, context, node) end def handle_request({:signature, hint}, context, _node) do @@ -30,11 +30,21 @@ defmodule Livebook.Intellisense.Erlang do |> Intellisense.Elixir.format_completion_identifiers() end - defp handle_details(line, _column, _context) do - # TODO: implement. See t:Livebook.Runtime.details_response/0 for return type. - IO.write("details:") - IO.inspect(line) - nil + defp handle_details(line, column, context, node) do + %{matches: matches, range: range} = + Intellisense.Erlang.IdentifierMatcher.locate_identifier(line, column, context, node) + + case Enum.filter(matches, &Intellisense.Elixir.include_in_details?/1) do + [] -> + nil + + matches -> + matches = Enum.sort_by(matches, & &1[:arity], :asc) + contents = Enum.map(matches, &Intellisense.Elixir.format_details_item/1) + + definition = Intellisense.Elixir.get_definition_location(hd(matches), context) + %{range: range, contents: contents, definition: definition} + end end defp handle_signature(hint, _context) do diff --git a/lib/livebook/intellisense/erlang/identifier_matcher.ex b/lib/livebook/intellisense/erlang/identifier_matcher.ex index 41b72669f6f..26eecb06841 100644 --- a/lib/livebook/intellisense/erlang/identifier_matcher.ex +++ b/lib/livebook/intellisense/erlang/identifier_matcher.ex @@ -31,6 +31,7 @@ defmodule Livebook.Intellisense.Erlang.IdentifierMatcher do @type name :: atom() @type display_name :: String.t() + @exact_matcher &Kernel.==/2 @prefix_matcher &String.starts_with?/2 def completion_identifiers(hint, intellisense_context, node) do @@ -47,10 +48,33 @@ defmodule Livebook.Intellisense.Erlang.IdentifierMatcher do context_to_matches(context, ctx) end + def locate_identifier(line, column, intellisense_context, node) do + case surround_context(line, column) do + %{context: context, begin: from, end: to} -> + fragment = String.slice(line, 0, to - 1) + + ctx = %{ + fragment: fragment, + intellisense_context: intellisense_context, + matcher: @exact_matcher, + type: :locate, + node: node + } + + matches = context_to_matches(context, ctx) + %{matches: matches, range: %{from: from, to: to}} + + :none -> + %{matches: [], range: nil} + end + end + defp context_to_matches(context, ctx) do case context do {:mod_func, mod, func} -> Intellisense.Elixir.IdentifierMatcher.match_module_function(mod, Atom.to_string(func), ctx) + {:mod, mod} -> + Intellisense.Elixir.IdentifierMatcher.match_erlang_module(Atom.to_string(mod), ctx) # TODO: all this: {:macro, macro} -> [] @@ -83,6 +107,44 @@ defmodule Livebook.Intellisense.Erlang.IdentifierMatcher do end end + defp surround_context(line, column) do + case :erl_scan.string(String.to_charlist(line)) do + {:error, _, _} -> + :none + {:ok, tokens, _} -> + before_cursor = split_tokens_at_column(tokens, column) + match_tokens_to_context_with_columns(before_cursor) + end + end + + defp split_tokens_at_column(tokens, column) do + {_, taken} = + Enum.reduce_while(tokens, {0, []}, fn token, {pos, acc} -> + text = token_to_text(token) + start_col = pos + 1 + + if start_col <= column do + new_pos = pos + String.length(text) + {:cont, {new_pos, [{token, start_col, new_pos + 1}| acc]}} + else + {:halt, {pos, acc}} + end + end) + taken + end + + defp token_to_text({_, _, val}), do: Atom.to_string(val) + defp token_to_text({val, _}), do: Atom.to_string(val) + + defp match_tokens_to_context_with_columns(tokens) do + case tokens do + [{{:atom, _, func}, _, to}, {{:":", _}, _, _}, {{:atom, _, mod}, from, _} | _] -> %{context: {:mod_func, mod, func}, begin: from, end: to} + [{{:atom, _, mod}, from, to} | _] -> %{context: {:mod, mod}, begin: from, end: to} + [] -> :none + _ -> :expr + end + end + defp match_tokens_to_context(tokens) do case tokens do [{:atom, _, func}, {:":", _}, {:atom, _, mod} | _] -> {:mod_func, mod, func} From 2e8359bad7b3b137bdc81c7dc3a86908cbe7f05f Mon Sep 17 00:00:00 2001 From: Filip Date: Sat, 27 Dec 2025 12:06:34 +0100 Subject: [PATCH 06/26] add docs for locate_identifier() --- .../intellisense/erlang/identifier_matcher.ex | 50 ++++++++++++------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/lib/livebook/intellisense/erlang/identifier_matcher.ex b/lib/livebook/intellisense/erlang/identifier_matcher.ex index 26eecb06841..a5ccc03292a 100644 --- a/lib/livebook/intellisense/erlang/identifier_matcher.ex +++ b/lib/livebook/intellisense/erlang/identifier_matcher.ex @@ -48,6 +48,18 @@ defmodule Livebook.Intellisense.Erlang.IdentifierMatcher do context_to_matches(context, ctx) end + @doc """ + Extracts information about an identifier found in `column` in + `line`. + + The function returns range of columns where the identifier + is located and a list of matching identifier items. + """ + @spec locate_identifier(String.t(), pos_integer(), Intellisense.context(), node()) :: + %{ + matches: list(identifier_item()), + range: nil | %{from: pos_integer(), to: pos_integer()} + } def locate_identifier(line, column, intellisense_context, node) do case surround_context(line, column) do %{context: context, begin: from, end: to} -> @@ -107,6 +119,25 @@ defmodule Livebook.Intellisense.Erlang.IdentifierMatcher do end end + defp match_tokens_to_context(tokens) do + case tokens do + [{:atom, _, func}, {:":", _}, {:atom, _, mod} | _] -> {:mod_func, mod, func} + [ {:":", _}, {:atom, _, mod} | _] -> {:mod_func, mod, :""} + + [{:atom, _, macro}, {:"?", _} | _] -> {:macro, macro} + [{:var, _, macro}, {:"?", _} | _] -> {:macro, macro} + + [{:atom, _, directive}, {:"-", _} | _] -> {:pre_directive, directive} + + [{:atom, _, atom} | _] -> {:atom, atom} + + [{:var, _, var} | _] -> {:var, var} + + [] -> :none + _ -> :expr + end + end + defp surround_context(line, column) do case :erl_scan.string(String.to_charlist(line)) do {:error, _, _} -> @@ -144,23 +175,4 @@ defmodule Livebook.Intellisense.Erlang.IdentifierMatcher do _ -> :expr end end - - defp match_tokens_to_context(tokens) do - case tokens do - [{:atom, _, func}, {:":", _}, {:atom, _, mod} | _] -> {:mod_func, mod, func} - [ {:":", _}, {:atom, _, mod} | _] -> {:mod_func, mod, :""} - - [{:atom, _, macro}, {:"?", _} | _] -> {:macro, macro} - [{:var, _, macro}, {:"?", _} | _] -> {:macro, macro} - - [{:atom, _, directive}, {:"-", _} | _] -> {:pre_directive, directive} - - [{:atom, _, atom} | _] -> {:atom, atom} - - [{:var, _, var} | _] -> {:var, var} - - [] -> :none - _ -> :expr - end - end end From 8f7c13dedcc987b1637233b338fa51e21e400dcc Mon Sep 17 00:00:00 2001 From: krotka Date: Mon, 5 Jan 2026 16:12:23 +0100 Subject: [PATCH 07/26] completion of erlang modules in erlang --- lib/livebook/intellisense/elixir/identifier_matcher.ex | 2 +- lib/livebook/intellisense/erlang.ex | 2 +- lib/livebook/intellisense/erlang/identifier_matcher.ex | 7 ++++++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/livebook/intellisense/elixir/identifier_matcher.ex b/lib/livebook/intellisense/elixir/identifier_matcher.ex index a091ca80dbe..913b8e2e332 100644 --- a/lib/livebook/intellisense/elixir/identifier_matcher.ex +++ b/lib/livebook/intellisense/elixir/identifier_matcher.ex @@ -519,7 +519,7 @@ defmodule Livebook.Intellisense.Elixir.IdentifierMatcher do do: %{item | display_name: "~" <> sigil_name} end - defp match_erlang_module(hint, ctx) do + def match_erlang_module(hint, ctx) do for mod <- get_matching_modules(hint, ctx), usable_as_unquoted_module?(mod), name = ":" <> Atom.to_string(mod), diff --git a/lib/livebook/intellisense/erlang.ex b/lib/livebook/intellisense/erlang.ex index 03431c7494a..fe732d5dbdb 100644 --- a/lib/livebook/intellisense/erlang.ex +++ b/lib/livebook/intellisense/erlang.ex @@ -27,7 +27,7 @@ defmodule Livebook.Intellisense.Erlang do IO.write("completion:") Intellisense.Erlang.IdentifierMatcher.completion_identifiers(hint, context, node) - |> Intellisense.Elixir.format_completion_identifiers() + |> Intellisense.Elixir.format_completion_identifiers() #TODO keywords, operators, booleans here end defp handle_details(line, _column, _context) do diff --git a/lib/livebook/intellisense/erlang/identifier_matcher.ex b/lib/livebook/intellisense/erlang/identifier_matcher.ex index 41b72669f6f..74f59e2db72 100644 --- a/lib/livebook/intellisense/erlang/identifier_matcher.ex +++ b/lib/livebook/intellisense/erlang/identifier_matcher.ex @@ -57,7 +57,7 @@ defmodule Livebook.Intellisense.Erlang.IdentifierMatcher do {:pre_directive, directive} -> [] {:atom, atom} -> - [] + match_atom(Atom.to_string(atom), ctx) {:var, var} -> var |> Livebook.Runtime.Evaluator.erlang_to_elixir_var @@ -101,4 +101,9 @@ defmodule Livebook.Intellisense.Erlang.IdentifierMatcher do _ -> :expr end end + + defp match_atom(hint, ctx) do + Intellisense.Elixir.IdentifierMatcher.match_erlang_module(hint, ctx) + |> Enum.map(&%{&1 | display_name: String.slice(&1[:display_name], 1..-1//1)}) + end end From 43b225dbfdd0fd2e24e4c844e2982f7733146581 Mon Sep 17 00:00:00 2001 From: AdrianGlanowski Date: Tue, 6 Jan 2026 18:57:34 +0100 Subject: [PATCH 08/26] hardcoded module attributes without docs --- .../intellisense/erlang/identifier_matcher.ex | 43 +++++++++++++++++-- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/lib/livebook/intellisense/erlang/identifier_matcher.ex b/lib/livebook/intellisense/erlang/identifier_matcher.ex index 41b72669f6f..0f59e5bd644 100644 --- a/lib/livebook/intellisense/erlang/identifier_matcher.ex +++ b/lib/livebook/intellisense/erlang/identifier_matcher.ex @@ -27,12 +27,38 @@ defmodule Livebook.Intellisense.Erlang.IdentifierMatcher do name: name(), arity: integer() } + | %{ + kind: :module_attribute, + name: name(), + documentation: Docs.documentation() + } @type name :: atom() @type display_name :: String.t() @prefix_matcher &String.starts_with?/2 + @reserved_attributes [ + {:module, %{doc: ""}}, + {:export, %{doc: ""}}, + {:import, %{doc: ""}}, + {:moduledoc, %{doc: ""}}, + {:compile, %{doc: ""}}, + {:vsn, %{doc: ""}}, + {:on_load, %{doc: ""}}, + {:nifs, %{doc: ""}}, + {:behaviour, %{doc: ""}}, + {:callback, %{doc: ""}}, + {:record, %{doc: ""}}, + {:include, %{doc: ""}}, + {:define, %{doc: ""}}, + {:file, %{doc: ""}}, + {:type, %{doc: ""}}, + {:spec, %{doc: ""}}, + {:doc, %{doc: ""}}, + {:feature, %{doc: ""}}, + ] + def completion_identifiers(hint, intellisense_context, node) do context = cursor_context(hint) @@ -52,10 +78,11 @@ defmodule Livebook.Intellisense.Erlang.IdentifierMatcher do {:mod_func, mod, func} -> Intellisense.Elixir.IdentifierMatcher.match_module_function(mod, Atom.to_string(func), ctx) # TODO: all this: - {:macro, macro} -> - [] + # {:macro, macro} -> + # [] {:pre_directive, directive} -> - [] + IO.inspect("DIRECTIVE!!!") + match_module_attribute(directive, ctx) {:atom, atom} -> [] {:var, var} -> @@ -101,4 +128,14 @@ defmodule Livebook.Intellisense.Erlang.IdentifierMatcher do _ -> :expr end end + + defp match_module_attribute(directive, ctx) do + for {attribute, info} <- @reserved_attributes, + ctx.matcher.(Atom.to_string(attribute), Atom.to_string(directive)), + do: %{ + kind: :module_attribute, + name: attribute, + documentation: {"text/markdown", info.doc} + } + end end From c7fcf161fcc43a5b8224599dd56559fd1abe8b06 Mon Sep 17 00:00:00 2001 From: AdrianGlanowski Date: Tue, 6 Jan 2026 19:41:34 +0100 Subject: [PATCH 09/26] styled way to complete erlang module attributes --- lib/livebook/intellisense/elixir.ex | 29 +++++++++++- .../intellisense/elixir/identifier_matcher.ex | 3 +- .../intellisense/erlang/identifier_matcher.ex | 45 ++++++++++--------- 3 files changed, 53 insertions(+), 24 deletions(-) diff --git a/lib/livebook/intellisense/elixir.ex b/lib/livebook/intellisense/elixir.ex index d4ba33be6bf..32aac2e9d43 100644 --- a/lib/livebook/intellisense/elixir.ex +++ b/lib/livebook/intellisense/elixir.ex @@ -197,14 +197,40 @@ defmodule Livebook.Intellisense.Elixir do insert_text: cond do arity == 0 -> "#{Atom.to_string(name)}()" + # true -> "#{Atom.to_string(name)}(${})" end } + + #Note: array_needed is a boolean to know if '[]' should be put inside atrribute, as in -export([]). It is also a way to differentiate erlang's atributes from elxir's. + defp format_completion_item(%{ + kind: :module_attribute, + name: name, + documentation: documentation, + array_needed: array_needed + }), + do: %{ + label: Atom.to_string(name), + kind: :variable, + documentation: + join_with_newlines([ + Intellisense.Elixir.Docs.format_documentation(documentation, :short), + "(module attribute)" + ]), + # A snippet with cursor in parentheses + insert_text: + if array_needed do + "#{name}([${}])." + else + "#{name}(${})." + end + } + defp format_completion_item(%{ kind: :module_attribute, name: name, - documentation: documentation + documentation: documentation, }), do: %{ label: Atom.to_string(name), @@ -217,6 +243,7 @@ defmodule Livebook.Intellisense.Elixir do insert_text: Atom.to_string(name) } + defp format_completion_item(%{kind: :bitstring_modifier, name: name, arity: arity}) do insert_text = if arity == 0 do diff --git a/lib/livebook/intellisense/elixir/identifier_matcher.ex b/lib/livebook/intellisense/elixir/identifier_matcher.ex index a091ca80dbe..681ce925dd7 100644 --- a/lib/livebook/intellisense/elixir/identifier_matcher.ex +++ b/lib/livebook/intellisense/elixir/identifier_matcher.ex @@ -65,7 +65,8 @@ defmodule Livebook.Intellisense.Elixir.IdentifierMatcher do | %{ kind: :module_attribute, name: name(), - documentation: Docs.documentation() + documentation: Docs.documentation(), + language: name() } | %{ kind: :bitstring_modifier, diff --git a/lib/livebook/intellisense/erlang/identifier_matcher.ex b/lib/livebook/intellisense/erlang/identifier_matcher.ex index 0f59e5bd644..41a7b9e1843 100644 --- a/lib/livebook/intellisense/erlang/identifier_matcher.ex +++ b/lib/livebook/intellisense/erlang/identifier_matcher.ex @@ -30,7 +30,8 @@ defmodule Livebook.Intellisense.Erlang.IdentifierMatcher do | %{ kind: :module_attribute, name: name(), - documentation: Docs.documentation() + documentation: Docs.documentation(), + array_needed: boolean() } @type name :: atom() @@ -39,24 +40,24 @@ defmodule Livebook.Intellisense.Erlang.IdentifierMatcher do @prefix_matcher &String.starts_with?/2 @reserved_attributes [ - {:module, %{doc: ""}}, - {:export, %{doc: ""}}, - {:import, %{doc: ""}}, - {:moduledoc, %{doc: ""}}, - {:compile, %{doc: ""}}, - {:vsn, %{doc: ""}}, - {:on_load, %{doc: ""}}, - {:nifs, %{doc: ""}}, - {:behaviour, %{doc: ""}}, - {:callback, %{doc: ""}}, - {:record, %{doc: ""}}, - {:include, %{doc: ""}}, - {:define, %{doc: ""}}, - {:file, %{doc: ""}}, - {:type, %{doc: ""}}, - {:spec, %{doc: ""}}, - {:doc, %{doc: ""}}, - {:feature, %{doc: ""}}, + {:module, %{doc: ""}, false}, + {:export, %{doc: ""}, true}, + {:import, %{doc: ""}, true}, + {:moduledoc, %{doc: ""}, false}, + {:compile, %{doc: ""}, true}, + {:vsn, %{doc: ""}, false}, + {:on_load, %{doc: ""}, false}, + {:nifs, %{doc: ""}, true}, + {:behaviour, %{doc: ""}, false}, + {:callback, %{doc: ""}, true}, + {:record, %{doc: ""}, false}, + {:include, %{doc: ""}, false}, + {:define, %{doc: ""}, false}, + {:file, %{doc: ""}, false}, + {:type, %{doc: ""}, false}, + {:spec, %{doc: ""}, false}, + {:doc, %{doc: ""}, false}, + {:feature, %{doc: ""}, false}, ] def completion_identifiers(hint, intellisense_context, node) do @@ -81,7 +82,6 @@ defmodule Livebook.Intellisense.Erlang.IdentifierMatcher do # {:macro, macro} -> # [] {:pre_directive, directive} -> - IO.inspect("DIRECTIVE!!!") match_module_attribute(directive, ctx) {:atom, atom} -> [] @@ -130,12 +130,13 @@ defmodule Livebook.Intellisense.Erlang.IdentifierMatcher do end defp match_module_attribute(directive, ctx) do - for {attribute, info} <- @reserved_attributes, + for {attribute, info, array_needed} <- @reserved_attributes, ctx.matcher.(Atom.to_string(attribute), Atom.to_string(directive)), do: %{ kind: :module_attribute, name: attribute, - documentation: {"text/markdown", info.doc} + documentation: {"text/markdown", info.doc}, + array_needed: array_needed, } end end From f4e621aae7470368c5d36702357604ecee47356f Mon Sep 17 00:00:00 2001 From: krotka Date: Wed, 7 Jan 2026 01:40:56 +0100 Subject: [PATCH 10/26] move erlang variable completion to a function --- .../intellisense/erlang/identifier_matcher.ex | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/livebook/intellisense/erlang/identifier_matcher.ex b/lib/livebook/intellisense/erlang/identifier_matcher.ex index 74f59e2db72..b3c86b17862 100644 --- a/lib/livebook/intellisense/erlang/identifier_matcher.ex +++ b/lib/livebook/intellisense/erlang/identifier_matcher.ex @@ -59,11 +59,7 @@ defmodule Livebook.Intellisense.Erlang.IdentifierMatcher do {:atom, atom} -> match_atom(Atom.to_string(atom), ctx) {:var, var} -> - var - |> Livebook.Runtime.Evaluator.erlang_to_elixir_var - |> to_string - |> Intellisense.Elixir.IdentifierMatcher.match_variable(ctx) - |> Enum.map(&%{&1 | name: Livebook.Runtime.Evaluator.elixir_to_erlang_var(&1[:name])}) + match_var(var, ctx) # TODO: bitstrings, need to be parsed! :expr -> [] @@ -106,4 +102,12 @@ defmodule Livebook.Intellisense.Erlang.IdentifierMatcher do Intellisense.Elixir.IdentifierMatcher.match_erlang_module(hint, ctx) |> Enum.map(&%{&1 | display_name: String.slice(&1[:display_name], 1..-1//1)}) end + + defp match_var(hint, ctx) do + hint + |> Livebook.Runtime.Evaluator.erlang_to_elixir_var + |> to_string + |> Intellisense.Elixir.IdentifierMatcher.match_variable(ctx) + |> Enum.map(&%{&1 | name: Livebook.Runtime.Evaluator.elixir_to_erlang_var(&1[:name])}) + end end From e98e82611809fd0be123a933890c89dc13bce577 Mon Sep 17 00:00:00 2001 From: krotka Date: Wed, 7 Jan 2026 04:00:35 +0100 Subject: [PATCH 11/26] keyword and operator completion in erlang --- lib/livebook/intellisense/erlang.ex | 55 ++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/lib/livebook/intellisense/erlang.ex b/lib/livebook/intellisense/erlang.ex index fe732d5dbdb..37888d896ac 100644 --- a/lib/livebook/intellisense/erlang.ex +++ b/lib/livebook/intellisense/erlang.ex @@ -27,7 +27,7 @@ defmodule Livebook.Intellisense.Erlang do IO.write("completion:") Intellisense.Erlang.IdentifierMatcher.completion_identifiers(hint, context, node) - |> Intellisense.Elixir.format_completion_identifiers() #TODO keywords, operators, booleans here + |> Intellisense.Elixir.format_completion_identifiers(extra_completion_items(hint)) end defp handle_details(line, _column, _context) do @@ -43,4 +43,57 @@ defmodule Livebook.Intellisense.Erlang do IO.inspect(hint) nil end + + @keywords [ + {"true", "(boolean)"}, + {"false", "(boolean)"}, + + {"begin", "(block operator)"}, + {"case", "(case operator)"}, + {"fun", "(anonymous function operator)"}, + {"if", "(if operator)"}, + {"when", "(guard operator)"}, + + {"after", "(after operator)"}, + {"catch", "(catch operator)"}, + {"receive", "(receive operator)"}, + {"try", "(try operator)"}, + + {"and", "(logical AND operator)"}, + {"andalso", "(short-circuit logical AND operator)"}, + {"band", "(bitwise AND operator)"}, + + {"not", "(logical NOT operator)"}, + {"bnot", "(bitwise NOT operator)"}, + + {"or", "(logical OR operator)"}, + {"orelse", "(short-circuit logical OR operator)"}, + {"bor", "(bitwise OR operator)"}, + + {"div", "(integer division operator)"}, + {"rem", "(integer remainder operator)"}, + {"bxor", "(bitwise XOR operator)"}, + {"bsl", "(bitshift left operator)"}, + {"bsr", "(bitshift right operator)"}, + {"xor", "(logical XOR operator)"}, + ] + + defp extra_completion_items(hint) do + items = Enum.map(@keywords, + fn {keyword, desc} -> %{ + label: keyword, + kind: :keyword, + documentation: desc, + insert_text: keyword, + } end + ) + + last_word = hint |> String.split(~r/\s/) |> List.last() + + if last_word == "" do + [] + else + Enum.filter(items, &String.starts_with?(&1.label, last_word)) + end + end end From 917cf6a7501797e9330f560bb8ef9ef18790670e Mon Sep 17 00:00:00 2001 From: krotka Date: Thu, 8 Jan 2026 20:29:15 +0100 Subject: [PATCH 12/26] bitstring modifier completion in erlang Not sure whether the heuristic here is the optimal one, but it seems to work quite well. --- .../intellisense/erlang/identifier_matcher.ex | 68 +++++++++++++++++-- 1 file changed, 64 insertions(+), 4 deletions(-) diff --git a/lib/livebook/intellisense/erlang/identifier_matcher.ex b/lib/livebook/intellisense/erlang/identifier_matcher.ex index b3c86b17862..29b2ee2e288 100644 --- a/lib/livebook/intellisense/erlang/identifier_matcher.ex +++ b/lib/livebook/intellisense/erlang/identifier_matcher.ex @@ -33,6 +33,24 @@ defmodule Livebook.Intellisense.Erlang.IdentifierMatcher do @prefix_matcher &String.starts_with?/2 + @bitstring_modifiers [ + :big, + :binary, + :bits, + :bitstring, + :bytes, + :integer, + :float, + :little, + :native, + :signed, + :unit, + :unsigned, + :utf8, + :utf16, + :utf32, + ] + def completion_identifiers(hint, intellisense_context, node) do context = cursor_context(hint) @@ -60,9 +78,11 @@ defmodule Livebook.Intellisense.Erlang.IdentifierMatcher do match_atom(Atom.to_string(atom), ctx) {:var, var} -> match_var(var, ctx) - # TODO: bitstrings, need to be parsed! - :expr -> - [] + {:bitstring_modifier, hint, existing} -> + for modifier <- @bitstring_modifiers, + @prefix_matcher.(Atom.to_string(modifier), Atom.to_string(hint)), + modifier not in existing, + do: %{kind: :bitstring_modifier, name: modifier, arity: 0} # :none _ -> @@ -87,7 +107,11 @@ defmodule Livebook.Intellisense.Erlang.IdentifierMatcher do [{:atom, _, macro}, {:"?", _} | _] -> {:macro, macro} [{:var, _, macro}, {:"?", _} | _] -> {:macro, macro} - [{:atom, _, directive}, {:"-", _} | _] -> {:pre_directive, directive} + [{:atom, _, directive}, {:"-", _}, {:".", _}] -> {:pre_directive, directive} + [{:atom, _, directive}, {:"-", _} ] -> {:pre_directive, directive} + + [{:atom, _, mod}, {:"-", _} | _] -> match_maybe_bitstring_mod(mod, tokens) + [{:atom, _, mod}, {:"/", _} | _] -> match_maybe_bitstring_mod(mod, tokens) [{:atom, _, atom} | _] -> {:atom, atom} @@ -110,4 +134,40 @@ defmodule Livebook.Intellisense.Erlang.IdentifierMatcher do |> Intellisense.Elixir.IdentifierMatcher.match_variable(ctx) |> Enum.map(&%{&1 | name: Livebook.Runtime.Evaluator.elixir_to_erlang_var(&1[:name])}) end + + defp match_maybe_bitstring_mod(hint, tokens) do + if in_bitstring?(tokens) do + existing = bitstring_mods(tokens) |> Enum.drop(1) + + case List.last(existing) do + {:err} -> {:atom, hint} + _ -> {:bitstring_modifier, hint, existing} + end + else + {:atom, hint} + end + end + + defp bitstring_mods(tokens) do + case tokens do + # the unit modifier takes an argument, we skip it here + [{:integer, _, _}, {:":", _}, {:atom, _, :unit} = head | tail] -> + bitstring_mods([head | tail]) + + [{:atom, _, mod}, {:"-", _} | tail] -> [mod | bitstring_mods(tail)] + [{:atom, _, mod}, {:"/", _} | _] -> [mod] + + _ -> [{:err}] + end + end + + defp in_bitstring?(tokens, depth \\ 0) do + case tokens do + [] -> false + [{:"<<", _} | _] when depth == 0 -> true + [{:"<<", _} | tail] -> in_bitstring?(tail, depth - 1) + [{:">>", _} | tail] -> in_bitstring?(tail, depth + 1) + [_ | tail] -> in_bitstring?(tail, depth) + end + end end From 4409e8759336f4b0327d9d9f955b7dd707d4f391 Mon Sep 17 00:00:00 2001 From: Filip Date: Fri, 9 Jan 2026 01:52:22 +0100 Subject: [PATCH 13/26] add BIF function and type completion and docs, as well as types in general --- .../intellisense/elixir/identifier_matcher.ex | 2 +- .../intellisense/erlang/identifier_matcher.ex | 20 ++++++++++--------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/lib/livebook/intellisense/elixir/identifier_matcher.ex b/lib/livebook/intellisense/elixir/identifier_matcher.ex index 913b8e2e332..d160e549aa8 100644 --- a/lib/livebook/intellisense/elixir/identifier_matcher.ex +++ b/lib/livebook/intellisense/elixir/identifier_matcher.ex @@ -360,7 +360,7 @@ defmodule Livebook.Intellisense.Elixir.IdentifierMatcher do Code.ensure_loaded?(mod) and function_exported?(mod, :exception, 1) end - defp match_module_member(mod, hint, ctx) do + def match_module_member(mod, hint, ctx) do match_module_function(mod, hint, ctx) ++ match_module_type(mod, hint, ctx) end diff --git a/lib/livebook/intellisense/erlang/identifier_matcher.ex b/lib/livebook/intellisense/erlang/identifier_matcher.ex index a5ccc03292a..961a051ede2 100644 --- a/lib/livebook/intellisense/erlang/identifier_matcher.ex +++ b/lib/livebook/intellisense/erlang/identifier_matcher.ex @@ -83,17 +83,15 @@ defmodule Livebook.Intellisense.Erlang.IdentifierMatcher do defp context_to_matches(context, ctx) do case context do - {:mod_func, mod, func} -> - Intellisense.Elixir.IdentifierMatcher.match_module_function(mod, Atom.to_string(func), ctx) - {:mod, mod} -> - Intellisense.Elixir.IdentifierMatcher.match_erlang_module(Atom.to_string(mod), ctx) + {:mod_member, mod, member} -> + Intellisense.Elixir.IdentifierMatcher.match_module_member(mod, Atom.to_string(member), ctx) # TODO: all this: {:macro, macro} -> [] {:pre_directive, directive} -> [] {:atom, atom} -> - [] + match_atom(Atom.to_string(atom), ctx) {:var, var} -> var |> Livebook.Runtime.Evaluator.erlang_to_elixir_var @@ -121,8 +119,8 @@ defmodule Livebook.Intellisense.Erlang.IdentifierMatcher do defp match_tokens_to_context(tokens) do case tokens do - [{:atom, _, func}, {:":", _}, {:atom, _, mod} | _] -> {:mod_func, mod, func} - [ {:":", _}, {:atom, _, mod} | _] -> {:mod_func, mod, :""} + [{:atom, _, member}, {:":", _}, {:atom, _, mod} | _] -> {:mod_member, mod, member} + [ {:":", _}, {:atom, _, mod} | _] -> {:mod_member, mod, :""} [{:atom, _, macro}, {:"?", _} | _] -> {:macro, macro} [{:var, _, macro}, {:"?", _} | _] -> {:macro, macro} @@ -138,6 +136,10 @@ defmodule Livebook.Intellisense.Erlang.IdentifierMatcher do end end + defp match_atom(hint, ctx) do + Intellisense.Elixir.IdentifierMatcher.match_erlang_module(hint, ctx) ++ Intellisense.Elixir.IdentifierMatcher.match_module_member(:erlang, hint, ctx) + end + defp surround_context(line, column) do case :erl_scan.string(String.to_charlist(line)) do {:error, _, _} -> @@ -169,8 +171,8 @@ defmodule Livebook.Intellisense.Erlang.IdentifierMatcher do defp match_tokens_to_context_with_columns(tokens) do case tokens do - [{{:atom, _, func}, _, to}, {{:":", _}, _, _}, {{:atom, _, mod}, from, _} | _] -> %{context: {:mod_func, mod, func}, begin: from, end: to} - [{{:atom, _, mod}, from, to} | _] -> %{context: {:mod, mod}, begin: from, end: to} + [{{:atom, _, member}, _, to}, {{:":", _}, _, _}, {{:atom, _, mod}, from, _} | _] -> %{context: {:mod_member, mod, member}, begin: from, end: to} + [{{:atom, _, atom}, from, to} | _] -> %{context: {:atom, atom}, begin: from, end: to} [] -> :none _ -> :expr end From 250f1f2feef5eeecc9c62f120bd3dcc360cddc07 Mon Sep 17 00:00:00 2001 From: Filip Date: Fri, 9 Jan 2026 23:35:41 +0100 Subject: [PATCH 14/26] add docs for erlang -docs module attribute --- lib/livebook/intellisense/elixir/docs.ex | 31 +++++++++++++++++-- .../intellisense/erlang/identifier_matcher.ex | 3 +- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/lib/livebook/intellisense/elixir/docs.ex b/lib/livebook/intellisense/elixir/docs.ex index 780609771c9..81bef3910ee 100644 --- a/lib/livebook/intellisense/elixir/docs.ex +++ b/lib/livebook/intellisense/elixir/docs.ex @@ -197,10 +197,35 @@ defmodule Livebook.Intellisense.Elixir.Docs do end end +# def locate_definition(path, {:function, name, arity}) do +# with {:ok, {:debug_info_v1, _, {:elixir_v1, meta, _}}} <- beam_lib_chunks(path, :debug_info), +# {_pair, _kind, kw, _body} <- keyfind(meta.definitions, {name, arity}) do +# Keyword.fetch(kw, :line) +# end +# end + def locate_definition(path, {:function, name, arity}) do - with {:ok, {:debug_info_v1, _, {:elixir_v1, meta, _}}} <- beam_lib_chunks(path, :debug_info), - {_pair, _kind, kw, _body} <- keyfind(meta.definitions, {name, arity}) do - Keyword.fetch(kw, :line) + case beam_lib_chunks(path, :debug_info) do + {:ok, {:debug_info_v1, _, {:elixir_v1, meta, _}}} -> + with {_pair, _kind, kw, _body} <- keyfind(meta.definitions, {name, arity}) do + Keyword.fetch(kw, :line) + end + _ -> + locate_erlang_function(path, name, arity) + end + end + + defp locate_erlang_function(path, name, arity) do + with {:ok, {:raw_abstract_v1, annotations}} <- beam_lib_chunks(path, :abstract_code) do + result = + Enum.find_value(annotations, fn + {:function, anno, ^name, ^arity, _} -> :erl_anno.line(anno) + _ -> nil + end) + + if result, do: {:ok, result}, else: :error + else + _ -> :error end end diff --git a/lib/livebook/intellisense/erlang/identifier_matcher.ex b/lib/livebook/intellisense/erlang/identifier_matcher.ex index 961a051ede2..3efc4962437 100644 --- a/lib/livebook/intellisense/erlang/identifier_matcher.ex +++ b/lib/livebook/intellisense/erlang/identifier_matcher.ex @@ -173,8 +173,7 @@ defmodule Livebook.Intellisense.Erlang.IdentifierMatcher do case tokens do [{{:atom, _, member}, _, to}, {{:":", _}, _, _}, {{:atom, _, mod}, from, _} | _] -> %{context: {:mod_member, mod, member}, begin: from, end: to} [{{:atom, _, atom}, from, to} | _] -> %{context: {:atom, atom}, begin: from, end: to} - [] -> :none - _ -> :expr + _ -> :none end end end From 743c8c3b9bdcee18753ae03735b5827e5bb5b9b7 Mon Sep 17 00:00:00 2001 From: Filip Date: Thu, 15 Jan 2026 17:08:36 +0100 Subject: [PATCH 15/26] fix for no record completion --- .../intellisense/erlang/identifier_matcher.ex | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/livebook/intellisense/erlang/identifier_matcher.ex b/lib/livebook/intellisense/erlang/identifier_matcher.ex index 3efc4962437..3fc4035ee67 100644 --- a/lib/livebook/intellisense/erlang/identifier_matcher.ex +++ b/lib/livebook/intellisense/erlang/identifier_matcher.ex @@ -122,6 +122,11 @@ defmodule Livebook.Intellisense.Erlang.IdentifierMatcher do [{:atom, _, member}, {:":", _}, {:atom, _, mod} | _] -> {:mod_member, mod, member} [ {:":", _}, {:atom, _, mod} | _] -> {:mod_member, mod, :""} + [{:atom, _, field}, {:".", _}, {:atom, _, record}, {:"#", _} | _] -> :none + [{:".", _}, {:atom, _, record}, {:"#", _} | _] -> :none + [{:atom, _, record}, {:"#", _} | _] -> :none + [{:"#", _} | _] -> :none + [{:atom, _, macro}, {:"?", _} | _] -> {:macro, macro} [{:var, _, macro}, {:"?", _} | _] -> {:macro, macro} @@ -137,7 +142,13 @@ defmodule Livebook.Intellisense.Erlang.IdentifierMatcher do end defp match_atom(hint, ctx) do - Intellisense.Elixir.IdentifierMatcher.match_erlang_module(hint, ctx) ++ Intellisense.Elixir.IdentifierMatcher.match_module_member(:erlang, hint, ctx) + (Intellisense.Elixir.IdentifierMatcher.match_erlang_module(hint, ctx) ++ Intellisense.Elixir.IdentifierMatcher.match_module_member(:erlang, hint, ctx)) + |> Enum.map(fn + %{display_name: name} = item when is_binary(name) -> + %{item | display_name: String.trim_leading(name, ":")} + item -> + item + end) end defp surround_context(line, column) do From 7b5ae6a31b0809147841ebf3873fe309e936648a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Dragon?= Date: Sat, 17 Jan 2026 08:59:41 +0100 Subject: [PATCH 16/26] Erlang Intellisense signature info Added function signature matching to erlang's intellisense --- lib/livebook/intellisense/elixir.ex | 3 +- .../intellisense/elixir/signature_matcher.ex | 3 +- lib/livebook/intellisense/erlang.ex | 25 +++-- .../intellisense/erlang/signature_matcher.ex | 92 +++++++++++++++++++ lib/livebook/runtime/erl_dist.ex | 1 + 5 files changed, 115 insertions(+), 9 deletions(-) create mode 100644 lib/livebook/intellisense/erlang/signature_matcher.ex diff --git a/lib/livebook/intellisense/elixir.ex b/lib/livebook/intellisense/elixir.ex index d4ba33be6bf..b53997110a6 100644 --- a/lib/livebook/intellisense/elixir.ex +++ b/lib/livebook/intellisense/elixir.ex @@ -475,7 +475,8 @@ defmodule Livebook.Intellisense.Elixir do end end - defp format_signature_item({_name, signature, _documentation, _specs}), + # FIXME: This is public + def format_signature_item({_name, signature, _documentation, _specs}), do: %{ signature: signature, arguments: arguments_from_signature(signature) diff --git a/lib/livebook/intellisense/elixir/signature_matcher.ex b/lib/livebook/intellisense/elixir/signature_matcher.ex index 6f981b0cbab..c4a58cd55c4 100644 --- a/lib/livebook/intellisense/elixir/signature_matcher.ex +++ b/lib/livebook/intellisense/elixir/signature_matcher.ex @@ -50,7 +50,8 @@ defmodule Livebook.Intellisense.Elixir.SignatureMatcher do end end - defp signature_infos_for_members(mod, funs, active_argument, node) do + # FIXME: This is public + def signature_infos_for_members(mod, funs, active_argument, node) do infos = Livebook.Intellisense.Elixir.Docs.lookup_module_members(mod, funs, node, kinds: [:function, :macro] diff --git a/lib/livebook/intellisense/erlang.ex b/lib/livebook/intellisense/erlang.ex index 03431c7494a..71bacd2990c 100644 --- a/lib/livebook/intellisense/erlang.ex +++ b/lib/livebook/intellisense/erlang.ex @@ -1,6 +1,7 @@ defmodule Livebook.Intellisense.Erlang do alias Livebook.Intellisense + alias Livebook.Intellisense.Elixir @behaviour Intellisense @@ -18,8 +19,8 @@ defmodule Livebook.Intellisense.Erlang do handle_details(line, column, context) end - def handle_request({:signature, hint}, context, _node) do - handle_signature(hint, context) + def handle_request({:signature, hint}, context, node) do + handle_signature(hint, context, node) end defp handle_completion(hint, context, node) do @@ -37,10 +38,20 @@ defmodule Livebook.Intellisense.Erlang do nil end - defp handle_signature(hint, _context) do - # TODO: implement. See t:Livebook.Runtime.signature_response/0 for return type. - IO.write("signature:") - IO.inspect(hint) - nil + defp handle_signature(hint, context, node) do + case Intellisense.Erlang.SignatureMatcher.get_matching_signatures(hint, context, node) do + {:ok, [], _active_argument} -> + nil + {:ok, signature_infos, active_argument} -> + %{ + active_argument: active_argument, + items: + signature_infos + |> Enum.map(&Intellisense.Elixir.format_signature_item/1) + |> Enum.uniq() + } + :error -> + nil + end end end diff --git a/lib/livebook/intellisense/erlang/signature_matcher.ex b/lib/livebook/intellisense/erlang/signature_matcher.ex new file mode 100644 index 00000000000..24c4b1d630c --- /dev/null +++ b/lib/livebook/intellisense/erlang/signature_matcher.ex @@ -0,0 +1,92 @@ +defmodule Livebook.Intellisense.Erlang.SignatureMatcher do + @type signature_info :: {name :: atom(), Docs.signature(), Docs.documentation(), Docs.spec()} + + + @spec get_matching_signatures(String.t(), Livebook.Intellisense.context(), node()) :: + {:ok, list(signature_info()), active_argument :: non_neg_integer()} | :error + def get_matching_signatures(hint, intellisense_context, node) do + %{env: env} = intellisense_context + + case call_target_and_argument(hint) do + {:ok, {:remote, mod, name}, active_argument} -> + funs = [{name, :any}] + signature_infos = Livebook.Intellisense.Elixir.SignatureMatcher.signature_infos_for_members(mod, funs, active_argument, node) + IO.inspect(signature_infos) + {:ok, signature_infos, active_argument} + {:ok, {:local, name}, active_argument} -> + imports = env.functions + + signature_infos = + imports + |> Enum.map(fn {mod, funs} -> + matching_funs = Enum.filter(funs, fn {fun, _arity} -> fun == name end) + {mod, matching_funs} + end) + |> Enum.reject(fn {_mod, matching_funs} -> matching_funs == [] end) + |> Enum.flat_map(fn {mod, matching_funs} -> + Livebook.Intellisense.Elixir.SignatureMatcher.signature_infos_for_members(mod, matching_funs, active_argument, node) + end) + IO.inspect(signature_infos) + {:ok, signature_infos, active_argument} + _ -> + :error + end + end + + defp call_target_and_argument(hint) do + with {:ok, ast} <- parse_last_call(hint) do + [call_head | _] = ast + case call_head do + {:call, _, {:remote, _, {:atom, _, mod}, {:atom, _, name}}, args} -> + {:ok, {:remote, mod, name}, length(args) - 1} + {:call, _, {:atom, _, name}, args} -> + {:ok, {:local, name}, length(args) - 1} + _ -> :error + end + else + _ -> :error + end + end + + defp parse_last_call(hint) do + case :erl_scan.string(String.to_charlist(hint)) do + {:ok, tokens, _} -> + tokens + |> Enum.reverse + |> filter_last_call + |> Enum.concat([{:atom, 1, :__context__}, {:")", 1}, {:dot, 1}]) + |> :erl_parse.parse_exprs + error -> + error + end + end + + defp filter_last_call(tokens) do + filter_last_call(tokens, :left_bracket) + end + defp filter_last_call([{:"(", 1} | tokens], :left_bracket) do + filter_last_call(tokens, :function_name) ++ [{:"(", 1}] + end + defp filter_last_call([{:")", 1} | tokens], :left_bracket) do + filter_last_call(tokens, :right_bracket) ++ [{:")", 1}] + end + defp filter_last_call([tok | tokens], :left_bracket) do + filter_last_call(tokens, :left_bracket) ++ [tok] + end + defp filter_last_call([{:"(", 1} | tokens], :right_bracket) do + filter_last_call(tokens, :left_bracket) ++ [{:"(", 1}] + end + defp filter_last_call([tok | tokens], :right_bracket) do + filter_last_call(tokens, :right_bracket) ++ [tok] + end + defp filter_last_call([{:atom, 1, fun}, {:":", 1}, {:atom, 1, mod} | _], :function_name) do + [{:atom, 1, mod}, {:":", 1}, {:atom, 1, fun}] + end + defp filter_last_call([{:atom, 1, fun} | _], :function_name) do + [{:atom, 1, fun}] + end + defp filter_last_call(_, _) do + [] + end + +end diff --git a/lib/livebook/runtime/erl_dist.ex b/lib/livebook/runtime/erl_dist.ex index 11feafac8fb..25a2da11cb3 100644 --- a/lib/livebook/runtime/erl_dist.ex +++ b/lib/livebook/runtime/erl_dist.ex @@ -37,6 +37,7 @@ defmodule Livebook.Runtime.ErlDist do Livebook.Intellisense.Elixir.SignatureMatcher, Livebook.Intellisense.Erlang, Livebook.Intellisense.Erlang.IdentifierMatcher, + Livebook.Intellisense.Erlang.SignatureMatcher, Livebook.Runtime.ErlDist, Livebook.Runtime.ErlDist.NodeManager, Livebook.Runtime.ErlDist.RuntimeServer, From 88b38d15470372c5e4d9b2ec9b48a69133d27739 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Dragon?= Date: Sat, 17 Jan 2026 14:39:34 +0100 Subject: [PATCH 17/26] Erlang Intellisense Signature Matching - Fixed BIF completion --- .../intellisense/erlang/signature_matcher.ex | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/lib/livebook/intellisense/erlang/signature_matcher.ex b/lib/livebook/intellisense/erlang/signature_matcher.ex index 24c4b1d630c..85ffb44de4a 100644 --- a/lib/livebook/intellisense/erlang/signature_matcher.ex +++ b/lib/livebook/intellisense/erlang/signature_matcher.ex @@ -14,19 +14,7 @@ defmodule Livebook.Intellisense.Erlang.SignatureMatcher do IO.inspect(signature_infos) {:ok, signature_infos, active_argument} {:ok, {:local, name}, active_argument} -> - imports = env.functions - - signature_infos = - imports - |> Enum.map(fn {mod, funs} -> - matching_funs = Enum.filter(funs, fn {fun, _arity} -> fun == name end) - {mod, matching_funs} - end) - |> Enum.reject(fn {_mod, matching_funs} -> matching_funs == [] end) - |> Enum.flat_map(fn {mod, matching_funs} -> - Livebook.Intellisense.Elixir.SignatureMatcher.signature_infos_for_members(mod, matching_funs, active_argument, node) - end) - IO.inspect(signature_infos) + signature_infos = Livebook.Intellisense.Elixir.SignatureMatcher.signature_infos_for_members(:erlang, [{name, :any}], active_argument, node) {:ok, signature_infos, active_argument} _ -> :error From 3681b71ad7e5ba8b85125ba5ace4f3dbdddfa024 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Dragon?= Date: Sat, 17 Jan 2026 14:54:17 +0100 Subject: [PATCH 18/26] Fixed merge errors - missing 'end' clause --- lib/livebook/intellisense/erlang/identifier_matcher.ex | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/livebook/intellisense/erlang/identifier_matcher.ex b/lib/livebook/intellisense/erlang/identifier_matcher.ex index 1a47d2c1b36..b6a5d84d2fe 100644 --- a/lib/livebook/intellisense/erlang/identifier_matcher.ex +++ b/lib/livebook/intellisense/erlang/identifier_matcher.ex @@ -57,7 +57,7 @@ defmodule Livebook.Intellisense.Erlang.IdentifierMatcher do :utf16, :utf32, ] - + @reserved_attributes [ {:module, %{doc: ""}, false}, {:export, %{doc: ""}, true}, @@ -197,6 +197,7 @@ defmodule Livebook.Intellisense.Erlang.IdentifierMatcher do documentation: {"text/markdown", info.doc}, array_needed: array_needed, } + end defp match_atom(hint, ctx) do (Intellisense.Elixir.IdentifierMatcher.match_erlang_module(hint, ctx) ++ Intellisense.Elixir.IdentifierMatcher.match_module_member(:erlang, hint, ctx)) @@ -244,7 +245,7 @@ defmodule Livebook.Intellisense.Erlang.IdentifierMatcher do _ -> :none end end - + defp match_var(hint, ctx) do hint |> Livebook.Runtime.Evaluator.erlang_to_elixir_var @@ -287,4 +288,5 @@ defmodule Livebook.Intellisense.Erlang.IdentifierMatcher do [{:">>", _} | tail] -> in_bitstring?(tail, depth + 1) [_ | tail] -> in_bitstring?(tail, depth) end + end end From b07f33e769a770a0e799fb3d77fed303a2612639 Mon Sep 17 00:00:00 2001 From: Filip Date: Sat, 17 Jan 2026 15:01:23 +0100 Subject: [PATCH 19/26] remove commented code --- lib/livebook/intellisense/elixir/docs.ex | 7 ------- 1 file changed, 7 deletions(-) diff --git a/lib/livebook/intellisense/elixir/docs.ex b/lib/livebook/intellisense/elixir/docs.ex index 81bef3910ee..81a3272c102 100644 --- a/lib/livebook/intellisense/elixir/docs.ex +++ b/lib/livebook/intellisense/elixir/docs.ex @@ -197,13 +197,6 @@ defmodule Livebook.Intellisense.Elixir.Docs do end end -# def locate_definition(path, {:function, name, arity}) do -# with {:ok, {:debug_info_v1, _, {:elixir_v1, meta, _}}} <- beam_lib_chunks(path, :debug_info), -# {_pair, _kind, kw, _body} <- keyfind(meta.definitions, {name, arity}) do -# Keyword.fetch(kw, :line) -# end -# end - def locate_definition(path, {:function, name, arity}) do case beam_lib_chunks(path, :debug_info) do {:ok, {:debug_info_v1, _, {:elixir_v1, meta, _}}} -> From c3c1cbf899c26731f7e0b46e2ee4f0c6e79214a4 Mon Sep 17 00:00:00 2001 From: krotka Date: Sat, 17 Jan 2026 15:06:22 +0100 Subject: [PATCH 20/26] remove debug prints and obsolete TODOs --- lib/livebook/intellisense/erlang.ex | 3 --- lib/livebook/intellisense/erlang/identifier_matcher.ex | 3 --- lib/livebook/intellisense/erlang/signature_matcher.ex | 1 - 3 files changed, 7 deletions(-) diff --git a/lib/livebook/intellisense/erlang.ex b/lib/livebook/intellisense/erlang.ex index de0be317985..d72f2a645dc 100644 --- a/lib/livebook/intellisense/erlang.ex +++ b/lib/livebook/intellisense/erlang.ex @@ -24,9 +24,6 @@ defmodule Livebook.Intellisense.Erlang do end defp handle_completion(hint, context, node) do - # TODO: implement. See t:Livebook.Runtime.completion_response/0 for return type. - IO.write("completion:") - Intellisense.Erlang.IdentifierMatcher.completion_identifiers(hint, context, node) |> Intellisense.Elixir.format_completion_identifiers(extra_completion_items(hint)) end diff --git a/lib/livebook/intellisense/erlang/identifier_matcher.ex b/lib/livebook/intellisense/erlang/identifier_matcher.ex index b6a5d84d2fe..1c23eb5d26a 100644 --- a/lib/livebook/intellisense/erlang/identifier_matcher.ex +++ b/lib/livebook/intellisense/erlang/identifier_matcher.ex @@ -130,9 +130,6 @@ defmodule Livebook.Intellisense.Erlang.IdentifierMatcher do case context do {:mod_member, mod, member} -> Intellisense.Elixir.IdentifierMatcher.match_module_member(mod, Atom.to_string(member), ctx) - # TODO: all this: - # {:macro, macro} -> - # [] {:pre_directive, directive} -> match_module_attribute(directive, ctx) {:atom, atom} -> diff --git a/lib/livebook/intellisense/erlang/signature_matcher.ex b/lib/livebook/intellisense/erlang/signature_matcher.ex index 85ffb44de4a..a68b4f7bc84 100644 --- a/lib/livebook/intellisense/erlang/signature_matcher.ex +++ b/lib/livebook/intellisense/erlang/signature_matcher.ex @@ -11,7 +11,6 @@ defmodule Livebook.Intellisense.Erlang.SignatureMatcher do {:ok, {:remote, mod, name}, active_argument} -> funs = [{name, :any}] signature_infos = Livebook.Intellisense.Elixir.SignatureMatcher.signature_infos_for_members(mod, funs, active_argument, node) - IO.inspect(signature_infos) {:ok, signature_infos, active_argument} {:ok, {:local, name}, active_argument} -> signature_infos = Livebook.Intellisense.Elixir.SignatureMatcher.signature_infos_for_members(:erlang, [{name, :any}], active_argument, node) From e6553bba59d125a49226abccf244905dbefa107a Mon Sep 17 00:00:00 2001 From: krotka Date: Sat, 17 Jan 2026 15:16:58 +0100 Subject: [PATCH 21/26] fix unused variable warnings --- lib/livebook/intellisense/erlang.ex | 1 - lib/livebook/intellisense/erlang/identifier_matcher.ex | 6 +++--- lib/livebook/intellisense/erlang/signature_matcher.ex | 4 +--- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/lib/livebook/intellisense/erlang.ex b/lib/livebook/intellisense/erlang.ex index d72f2a645dc..eb975af6d0f 100644 --- a/lib/livebook/intellisense/erlang.ex +++ b/lib/livebook/intellisense/erlang.ex @@ -1,7 +1,6 @@ defmodule Livebook.Intellisense.Erlang do alias Livebook.Intellisense - alias Livebook.Intellisense.Elixir @behaviour Intellisense diff --git a/lib/livebook/intellisense/erlang/identifier_matcher.ex b/lib/livebook/intellisense/erlang/identifier_matcher.ex index 1c23eb5d26a..7c6935bb1e2 100644 --- a/lib/livebook/intellisense/erlang/identifier_matcher.ex +++ b/lib/livebook/intellisense/erlang/identifier_matcher.ex @@ -162,9 +162,9 @@ defmodule Livebook.Intellisense.Erlang.IdentifierMatcher do [{:atom, _, member}, {:":", _}, {:atom, _, mod} | _] -> {:mod_member, mod, member} [ {:":", _}, {:atom, _, mod} | _] -> {:mod_member, mod, :""} - [{:atom, _, field}, {:".", _}, {:atom, _, record}, {:"#", _} | _] -> :none - [{:".", _}, {:atom, _, record}, {:"#", _} | _] -> :none - [{:atom, _, record}, {:"#", _} | _] -> :none + [{:atom, _, _field}, {:".", _}, {:atom, _, _record}, {:"#", _} | _] -> :none + [{:".", _}, {:atom, _, _record}, {:"#", _} | _] -> :none + [{:atom, _, _record}, {:"#", _} | _] -> :none [{:"#", _} | _] -> :none [{:atom, _, macro}, {:"?", _} | _] -> {:macro, macro} diff --git a/lib/livebook/intellisense/erlang/signature_matcher.ex b/lib/livebook/intellisense/erlang/signature_matcher.ex index a68b4f7bc84..df3b6049c3a 100644 --- a/lib/livebook/intellisense/erlang/signature_matcher.ex +++ b/lib/livebook/intellisense/erlang/signature_matcher.ex @@ -4,9 +4,7 @@ defmodule Livebook.Intellisense.Erlang.SignatureMatcher do @spec get_matching_signatures(String.t(), Livebook.Intellisense.context(), node()) :: {:ok, list(signature_info()), active_argument :: non_neg_integer()} | :error - def get_matching_signatures(hint, intellisense_context, node) do - %{env: env} = intellisense_context - + def get_matching_signatures(hint, _intellisense_context, node) do case call_target_and_argument(hint) do {:ok, {:remote, mod, name}, active_argument} -> funs = [{name, :any}] From add29c73f2cae024ec60914967a47e5d3736bff3 Mon Sep 17 00:00:00 2001 From: krotka Date: Sat, 17 Jan 2026 15:26:39 +0100 Subject: [PATCH 22/26] fix function clause grouping error --- lib/livebook/intellisense/elixir/docs.ex | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/lib/livebook/intellisense/elixir/docs.ex b/lib/livebook/intellisense/elixir/docs.ex index 81bef3910ee..2885fe2d796 100644 --- a/lib/livebook/intellisense/elixir/docs.ex +++ b/lib/livebook/intellisense/elixir/docs.ex @@ -197,13 +197,6 @@ defmodule Livebook.Intellisense.Elixir.Docs do end end -# def locate_definition(path, {:function, name, arity}) do -# with {:ok, {:debug_info_v1, _, {:elixir_v1, meta, _}}} <- beam_lib_chunks(path, :debug_info), -# {_pair, _kind, kw, _body} <- keyfind(meta.definitions, {name, arity}) do -# Keyword.fetch(kw, :line) -# end -# end - def locate_definition(path, {:function, name, arity}) do case beam_lib_chunks(path, :debug_info) do {:ok, {:debug_info_v1, _, {:elixir_v1, meta, _}}} -> @@ -215,6 +208,12 @@ defmodule Livebook.Intellisense.Elixir.Docs do end end + def locate_definition(path, {:type, name, arity}) do + with {:ok, {:raw_abstract_v1, annotations}} <- beam_lib_chunks(path, :abstract_code) do + fetch_type_line(annotations, name, arity) + end + end + defp locate_erlang_function(path, name, arity) do with {:ok, {:raw_abstract_v1, annotations}} <- beam_lib_chunks(path, :abstract_code) do result = @@ -229,12 +228,6 @@ defmodule Livebook.Intellisense.Elixir.Docs do end end - def locate_definition(path, {:type, name, arity}) do - with {:ok, {:raw_abstract_v1, annotations}} <- beam_lib_chunks(path, :abstract_code) do - fetch_type_line(annotations, name, arity) - end - end - defp fetch_type_line(annotations, name, arity) do for {:attribute, anno, :type, {^name, _, vars}} <- annotations, length(vars) == arity do :erl_anno.line(anno) From f20bef24419fdd57afa2df90c2e1ebed45643747 Mon Sep 17 00:00:00 2001 From: krotka Date: Sat, 17 Jan 2026 15:47:21 +0100 Subject: [PATCH 23/26] fix formatting --- lib/livebook/intellisense/elixir.ex | 3 ++- .../intellisense/erlang/identifier_matcher.ex | 21 ++++++++++------- .../intellisense/erlang/signature_matcher.ex | 23 ++++++++++++++++--- 3 files changed, 35 insertions(+), 12 deletions(-) diff --git a/lib/livebook/intellisense/elixir.ex b/lib/livebook/intellisense/elixir.ex index 50fd11fe440..40ed739a2ad 100644 --- a/lib/livebook/intellisense/elixir.ex +++ b/lib/livebook/intellisense/elixir.ex @@ -203,7 +203,8 @@ defmodule Livebook.Intellisense.Elixir do } - #Note: array_needed is a boolean to know if '[]' should be put inside atrribute, as in -export([]). It is also a way to differentiate erlang's atributes from elxir's. + # Note: array_needed is a boolean to know if '[]' should be put inside atrribute, + # as in -export([]). It is also a way to differentiate erlang's atributes from elixir's. defp format_completion_item(%{ kind: :module_attribute, name: name, diff --git a/lib/livebook/intellisense/erlang/identifier_matcher.ex b/lib/livebook/intellisense/erlang/identifier_matcher.ex index 7c6935bb1e2..782a12726e3 100644 --- a/lib/livebook/intellisense/erlang/identifier_matcher.ex +++ b/lib/livebook/intellisense/erlang/identifier_matcher.ex @@ -197,13 +197,14 @@ defmodule Livebook.Intellisense.Erlang.IdentifierMatcher do end defp match_atom(hint, ctx) do - (Intellisense.Elixir.IdentifierMatcher.match_erlang_module(hint, ctx) ++ Intellisense.Elixir.IdentifierMatcher.match_module_member(:erlang, hint, ctx)) + (Intellisense.Elixir.IdentifierMatcher.match_erlang_module(hint, ctx) ++ + Intellisense.Elixir.IdentifierMatcher.match_module_member(:erlang, hint, ctx)) |> Enum.map(fn - %{display_name: name} = item when is_binary(name) -> - %{item | display_name: String.trim_leading(name, ":")} - item -> - item - end) + %{display_name: name} = item when is_binary(name) -> + %{item | display_name: String.trim_leading(name, ":")} + item -> + item + end) end defp surround_context(line, column) do @@ -237,8 +238,12 @@ defmodule Livebook.Intellisense.Erlang.IdentifierMatcher do defp match_tokens_to_context_with_columns(tokens) do case tokens do - [{{:atom, _, member}, _, to}, {{:":", _}, _, _}, {{:atom, _, mod}, from, _} | _] -> %{context: {:mod_member, mod, member}, begin: from, end: to} - [{{:atom, _, atom}, from, to} | _] -> %{context: {:atom, atom}, begin: from, end: to} + [{{:atom, _, member}, _, to}, {{:":", _}, _, _}, {{:atom, _, mod}, from, _} | _] -> + %{context: {:mod_member, mod, member}, begin: from, end: to} + + [{{:atom, _, atom}, from, to} | _] -> + %{context: {:atom, atom}, begin: from, end: to} + _ -> :none end end diff --git a/lib/livebook/intellisense/erlang/signature_matcher.ex b/lib/livebook/intellisense/erlang/signature_matcher.ex index df3b6049c3a..34953c0adf4 100644 --- a/lib/livebook/intellisense/erlang/signature_matcher.ex +++ b/lib/livebook/intellisense/erlang/signature_matcher.ex @@ -1,4 +1,6 @@ defmodule Livebook.Intellisense.Erlang.SignatureMatcher do + alias Livebook.Intellisense + @type signature_info :: {name :: atom(), Docs.signature(), Docs.documentation(), Docs.spec()} @@ -7,12 +9,27 @@ defmodule Livebook.Intellisense.Erlang.SignatureMatcher do def get_matching_signatures(hint, _intellisense_context, node) do case call_target_and_argument(hint) do {:ok, {:remote, mod, name}, active_argument} -> - funs = [{name, :any}] - signature_infos = Livebook.Intellisense.Elixir.SignatureMatcher.signature_infos_for_members(mod, funs, active_argument, node) + signature_infos = + Intellisense.Elixir.SignatureMatcher.signature_infos_for_members( + mod, + [{name, :any}], + active_argument, + node + ) + {:ok, signature_infos, active_argument} + {:ok, {:local, name}, active_argument} -> - signature_infos = Livebook.Intellisense.Elixir.SignatureMatcher.signature_infos_for_members(:erlang, [{name, :any}], active_argument, node) + signature_infos = + Intellisense.Elixir.SignatureMatcher.signature_infos_for_members( + :erlang, + [{name, :any}], + active_argument, + node + ) + {:ok, signature_infos, active_argument} + _ -> :error end From 55850d2d33a5eca1bab8d3ff023ec6c737641602 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20K=C5=82osko?= Date: Wed, 11 Feb 2026 19:12:44 +0100 Subject: [PATCH 24/26] Compile Erlang modules with debug_info # Conflicts: # lib/livebook/intellisense/elixir/docs.ex --- lib/livebook/intellisense/elixir.ex | 4 +-- lib/livebook/intellisense/elixir/docs.ex | 33 ++++++++++++++---------- lib/livebook/runtime/evaluator.ex | 2 +- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/lib/livebook/intellisense/elixir.ex b/lib/livebook/intellisense/elixir.ex index 40ed739a2ad..a9c04c66c9c 100644 --- a/lib/livebook/intellisense/elixir.ex +++ b/lib/livebook/intellisense/elixir.ex @@ -474,8 +474,8 @@ defmodule Livebook.Intellisense.Elixir do with true <- File.exists?(path), {:ok, line} <- - Intellisense.Elixir.Docs.locate_definition(String.to_charlist(path), identifier) do - file = module.module_info(:compile)[:source] + Intellisense.Elixir.Docs.locate_definition(String.to_charlist(path), identifier), + {:ok, file} <- Keyword.fetch(module.module_info(:compile), :source) do %{file: to_string(file), line: line} else _otherwise -> nil diff --git a/lib/livebook/intellisense/elixir/docs.ex b/lib/livebook/intellisense/elixir/docs.ex index 2885fe2d796..3b7b0df7052 100644 --- a/lib/livebook/intellisense/elixir/docs.ex +++ b/lib/livebook/intellisense/elixir/docs.ex @@ -189,28 +189,35 @@ defmodule Livebook.Intellisense.Elixir.Docs do def locate_definition(path, identifier) def locate_definition(path, {:module, module}) do - with {:ok, {:raw_abstract_v1, annotations}} <- beam_lib_chunks(path, :abstract_code) do - {:attribute, anno, :module, ^module} = - Enum.find(annotations, &match?({:attribute, _, :module, _}, &1)) + case beam_lib_chunks(path, :abstract_code) do + {:ok, {:raw_abstract_v1, annotations}} -> + {:attribute, anno, :module, ^module} = + Enum.find(annotations, &match?({:attribute, _, :module, _}, &1)) + + {:ok, :erl_anno.line(anno)} - {:ok, :erl_anno.line(anno)} + _ -> + :error end end def locate_definition(path, {:function, name, arity}) do - case beam_lib_chunks(path, :debug_info) do - {:ok, {:debug_info_v1, _, {:elixir_v1, meta, _}}} -> - with {_pair, _kind, kw, _body} <- keyfind(meta.definitions, {name, arity}) do - Keyword.fetch(kw, :line) - end - _ -> - locate_erlang_function(path, name, arity) + with {:ok, {:debug_info_v1, _, {:elixir_v1, meta, _}}} <- beam_lib_chunks(path, :debug_info), + {_pair, _kind, kw, _body} <- keyfind(meta.definitions, {name, arity}) do + Keyword.fetch(kw, :line) + #TODO: maybe we should create separate function flow for erlang, or at least handle case where both fail with :error here + else + _ -> locate_erlang_function(path, name, arity) end end def locate_definition(path, {:type, name, arity}) do - with {:ok, {:raw_abstract_v1, annotations}} <- beam_lib_chunks(path, :abstract_code) do - fetch_type_line(annotations, name, arity) + case beam_lib_chunks(path, :abstract_code) do + {:ok, {:raw_abstract_v1, annotations}} -> + fetch_type_line(annotations, name, arity) + + _ -> + :error end end diff --git a/lib/livebook/runtime/evaluator.ex b/lib/livebook/runtime/evaluator.ex index c90ff662574..246363044a9 100644 --- a/lib/livebook/runtime/evaluator.ex +++ b/lib/livebook/runtime/evaluator.ex @@ -751,7 +751,7 @@ defmodule Livebook.Runtime.Evaluator do try do {:ok, forms} = :epp.parse_file(filename, source_name: String.to_charlist(env.file)) - case :compile.forms(forms) do + case :compile.forms(forms, [:debug_info]) do {:ok, module, binary} -> file = if ebin_path = ebin_path() do From 953c6f196d0d1247d65d2981742674bb6dcb5bfc Mon Sep 17 00:00:00 2001 From: Filip Date: Sun, 15 Feb 2026 16:32:01 +0100 Subject: [PATCH 25/26] change signature and spec formatting to erlang style --- lib/livebook/intellisense/elixir.ex | 38 ++- lib/livebook/intellisense/elixir/docs.ex | 1 + lib/livebook/intellisense/erlang.ex | 310 +++++++++++++++++- .../intellisense/erlang/identifier_matcher.ex | 4 +- 4 files changed, 332 insertions(+), 21 deletions(-) diff --git a/lib/livebook/intellisense/elixir.ex b/lib/livebook/intellisense/elixir.ex index a9c04c66c9c..931ac4c5a56 100644 --- a/lib/livebook/intellisense/elixir.ex +++ b/lib/livebook/intellisense/elixir.ex @@ -55,9 +55,9 @@ defmodule Livebook.Intellisense.Elixir do %{items: items} end - defp include_in_completion?(%{kind: :module, documentation: :hidden}), do: false - defp include_in_completion?(%{kind: :function, documentation: :hidden}), do: false - defp include_in_completion?(_), do: true + def include_in_completion?(%{kind: :module, documentation: :hidden}), do: false + def include_in_completion?(%{kind: :function, documentation: :hidden}), do: false + def include_in_completion?(_), do: true defp format_completion_item(%{kind: :variable, name: name}), do: %{ @@ -138,6 +138,7 @@ defmodule Livebook.Intellisense.Elixir do } end + #TODO : module and signature is important in display of completion docs, name and displayname isnt in happy flow, if no signatures name is also needed defp format_completion_item(%{ kind: :function, module: module, @@ -261,7 +262,7 @@ defmodule Livebook.Intellisense.Elixir do } end - defp keyword_macro?(name) do + def keyword_macro?(name) do def? = name |> Atom.to_string() |> String.starts_with?("def") def? or @@ -289,7 +290,7 @@ defmodule Livebook.Intellisense.Elixir do ] end - defp env_macro?(name) do + def env_macro?(name) do name in [:__ENV__, :__MODULE__, :__DIR__, :__STACKTRACE__, :__CALLER__] end @@ -342,7 +343,7 @@ defmodule Livebook.Intellisense.Elixir do :bitstring_option ] - defp completion_item_priority(%{kind: :struct} = completion_item) do + def completion_item_priority(%{kind: :struct} = completion_item) do if completion_item.documentation =~ "(exception)" do {length(@ordered_kinds), completion_item.label} else @@ -350,7 +351,7 @@ defmodule Livebook.Intellisense.Elixir do end end - defp completion_item_priority(completion_item) do + def completion_item_priority(completion_item) do {completion_item_kind_priority(completion_item.kind), completion_item.label} end @@ -398,6 +399,7 @@ defmodule Livebook.Intellisense.Elixir do ]) end + #TODO: module formatting should handle erlang modules too, module is of type :math and should be math def format_details_item(%{kind: :module, module: module, documentation: documentation}) do join_with_divider([ code(inspect(module)), @@ -406,6 +408,7 @@ defmodule Livebook.Intellisense.Elixir do ]) end + #TODO: format_signatures and format_specs needs to be reworked for erlang functions def format_details_item(%{ kind: :function, module: module, @@ -518,22 +521,22 @@ defmodule Livebook.Intellisense.Elixir do # Formatting helpers - defp join_with_divider(strings), do: join_with(strings, "\n\n---\n\n") + def join_with_divider(strings), do: join_with(strings, "\n\n---\n\n") - defp join_with_newlines(strings), do: join_with(strings, "\n\n") + def join_with_newlines(strings), do: join_with(strings, "\n\n") - defp join_with_middle_dot(strings), do: join_with(strings, " · ") + def join_with_middle_dot(strings), do: join_with(strings, " · ") - defp join_with(strings, joiner) do + def join_with(strings, joiner) do case Enum.reject(strings, &is_nil/1) do [] -> nil parts -> Enum.join(parts, joiner) end end - defp code(nil), do: nil + def code(nil), do: nil - defp code(code) do + def code(code) do """ ``` #{code} @@ -541,7 +544,7 @@ defmodule Livebook.Intellisense.Elixir do """ end - defp format_docs_link(module, function_or_type \\ nil) do + def format_docs_link(module, function_or_type \\ nil) do app = Application.get_application(module) module_name = module_name(module) @@ -591,6 +594,7 @@ defmodule Livebook.Intellisense.Elixir do signature_fallback(module, name, arity) end + #TODO: this should be reimplemented for erlang, module is of type :module, and . is used not : defp format_signatures(signatures, module, _name, _arity) do signatures_string = Enum.join(signatures, "\n") @@ -616,15 +620,15 @@ defmodule Livebook.Intellisense.Elixir do "#{inspect(module)}.#{name}(#{args})" end - defp format_meta(:deprecated, %{deprecated: deprecated}) do + def format_meta(:deprecated, %{deprecated: deprecated}) do "**Deprecated**. " <> deprecated end - defp format_meta(:since, %{since: since}) do + def format_meta(:since, %{since: since}) do "Since " <> since end - defp format_meta(_, _), do: nil + def format_meta(_, _), do: nil defp format_specs([], _name, _line_length), do: nil diff --git a/lib/livebook/intellisense/elixir/docs.ex b/lib/livebook/intellisense/elixir/docs.ex index 3b7b0df7052..576b59d2db3 100644 --- a/lib/livebook/intellisense/elixir/docs.ex +++ b/lib/livebook/intellisense/elixir/docs.ex @@ -211,6 +211,7 @@ defmodule Livebook.Intellisense.Elixir.Docs do end end + #TODO: maybe we should locate types for erlang too, not only functions def locate_definition(path, {:type, name, arity}) do case beam_lib_chunks(path, :abstract_code) do {:ok, {:raw_abstract_v1, annotations}} -> diff --git a/lib/livebook/intellisense/erlang.ex b/lib/livebook/intellisense/erlang.ex index eb975af6d0f..244d6c0f0c3 100644 --- a/lib/livebook/intellisense/erlang.ex +++ b/lib/livebook/intellisense/erlang.ex @@ -4,6 +4,8 @@ defmodule Livebook.Intellisense.Erlang do @behaviour Intellisense + @line_length 45 + @impl true def handle_request({:format, _code}, _context, _node) do # Not supported. @@ -24,7 +26,179 @@ defmodule Livebook.Intellisense.Erlang do defp handle_completion(hint, context, node) do Intellisense.Erlang.IdentifierMatcher.completion_identifiers(hint, context, node) - |> Intellisense.Elixir.format_completion_identifiers(extra_completion_items(hint)) + |> format_completion_identifiers(extra_completion_items(hint)) + end + + def format_completion_identifiers(completions, extra \\ []) do + items = + completions + |> Enum.filter(&Intellisense.Elixir.include_in_completion?/1) + |> Enum.map(&format_completion_item/1) + |> Enum.concat(extra) + |> Enum.sort_by(&Intellisense.Elixir.completion_item_priority/1) + + %{items: items} + end + + defp format_completion_item(%{kind: :variable, name: name}), + do: %{ + label: Atom.to_string(name), + kind: :variable, + documentation: "(variable)", + insert_text: Atom.to_string(name) + } + + defp format_completion_item(%{ + kind: :module, + module: module, + display_name: display_name, + documentation: documentation + }) do + subtype = Intellisense.Elixir.Docs.get_module_subtype(module) + + kind = + case subtype do + :protocol -> :interface + :exception -> :struct + :struct -> :struct + :behaviour -> :interface + _ -> :module + end + + detail = Atom.to_string(subtype || :module) + + %{ + label: display_name, + kind: kind, + documentation: + Intellisense.Elixir.join_with_newlines([ + Intellisense.Elixir.Docs.format_documentation(documentation, :short), + "(#{detail})" + ]), + insert_text: String.trim_leading(display_name, ":") + } + end + + defp format_completion_item(%{ + kind: :function, + module: module, + name: name, + arity: arity, + type: type, + display_name: display_name, + documentation: documentation, + signatures: signatures + }), + do: %{ + label: "#{display_name}/#{arity}", + kind: :function, + documentation: + Intellisense.Elixir.join_with_newlines([ + Intellisense.Elixir.Docs.format_documentation(documentation, :short), + Intellisense.Elixir.code(format_signatures(signatures, module, name, arity)) + ]), + insert_text: + cond do + type == :macro and Intellisense.Elixir.keyword_macro?(name) -> + "#{display_name} " + + type == :macro and Intellisense.Elixir.env_macro?(name) -> + display_name + + String.starts_with?(display_name, "~") -> + display_name + + Macro.operator?(name, arity) -> + display_name + + arity == 0 -> + "#{display_name}()" + + true -> + # A snippet with cursor in parentheses + "#{display_name}(${})" + end + } + + defp format_completion_item(%{ + kind: :type, + name: name, + arity: arity, + documentation: documentation, + type_spec: type_spec + }), + do: %{ + label: "#{name}/#{arity}", + kind: :type, + documentation: + Intellisense.Elixir.join_with_newlines([ + Intellisense.Elixir.Docs.format_documentation(documentation, :short), + format_type_spec(type_spec) |> Intellisense.Elixir.code() + ]), + insert_text: + cond do + arity == 0 -> "#{Atom.to_string(name)}()" + # + true -> "#{Atom.to_string(name)}(${})" + end + } + + + # Note: array_needed is a boolean to know if '[]' should be put inside atrribute, + # as in -export([]). It is also a way to differentiate erlang's atributes from elixir's. + defp format_completion_item(%{ + kind: :module_attribute, + name: name, + documentation: documentation, + array_needed: array_needed + }), + do: %{ + label: Atom.to_string(name), + kind: :variable, + documentation: + Intellisense.Elixir.join_with_newlines([ + Intellisense.Elixir.Docs.format_documentation(documentation, :short), + "(module attribute)" + ]), + # A snippet with cursor in parentheses + insert_text: + if array_needed do + "#{name}([${}])." + else + "#{name}(${})." + end + } + + defp format_completion_item(%{ + kind: :module_attribute, + name: name, + documentation: documentation, + }), + do: %{ + label: Atom.to_string(name), + kind: :variable, + documentation: + Intellisense.Elixir.join_with_newlines([ + Intellisense.Elixir.Docs.format_documentation(documentation, :short), + "(module attribute)" + ]), + insert_text: Atom.to_string(name) + } + + defp format_completion_item(%{kind: :bitstring_modifier, name: name, arity: arity}) do + insert_text = + if arity == 0 do + Atom.to_string(name) + else + "#{name}(${})" + end + + %{ + label: Atom.to_string(name), + kind: :type, + documentation: "(bitstring option)", + insert_text: insert_text + } end defp handle_details(line, column, context, node) do @@ -37,13 +211,59 @@ defmodule Livebook.Intellisense.Erlang do matches -> matches = Enum.sort_by(matches, & &1[:arity], :asc) - contents = Enum.map(matches, &Intellisense.Elixir.format_details_item/1) + contents = Enum.map(matches, &format_details_item/1) definition = Intellisense.Elixir.get_definition_location(hd(matches), context) %{range: range, contents: contents, definition: definition} end end + def format_details_item(%{kind: :module, module: module, documentation: documentation}) do + Intellisense.Elixir.join_with_divider([ + Intellisense.Elixir.code(Atom.to_string(module)), + Intellisense.Elixir.format_docs_link(module), + Intellisense.Elixir.Docs.format_documentation(documentation, :all) + ]) + end + + def format_details_item(%{ + kind: :function, + module: module, + name: name, + arity: arity, + documentation: documentation, + signatures: signatures, + specs: specs, + meta: meta + }) do + Intellisense.Elixir.join_with_divider([ + format_signatures(signatures, module, name, arity) |> Intellisense.Elixir.code(), + Intellisense.Elixir.join_with_middle_dot([ + Intellisense.Elixir.format_docs_link(module, {:function, name, arity}), + Intellisense.Elixir.format_meta(:since, meta) + ]), + Intellisense.Elixir.format_meta(:deprecated, meta), + format_specs(specs, name, arity) |> Intellisense.Elixir.code(), + Intellisense.Elixir.Docs.format_documentation(documentation, :all) + ]) + end + + def format_details_item(%{ + kind: :type, + module: module, + name: name, + arity: arity, + documentation: documentation, + type_spec: type_spec + }) do + Intellisense.Elixir.join_with_divider([ + format_type_signature(type_spec, module, name, arity) |> Intellisense.Elixir.code(), + Intellisense.Elixir.format_docs_link(module, {:type, name, arity}), + format_type_spec(type_spec) |> Intellisense.Elixir.code(), + Intellisense.Elixir.Docs.format_documentation(documentation, :all) + ]) + end + defp handle_signature(hint, context, node) do case Intellisense.Erlang.SignatureMatcher.get_matching_signatures(hint, context, node) do {:ok, [], _active_argument} -> @@ -61,6 +281,92 @@ defmodule Livebook.Intellisense.Erlang do end end + defp format_signatures([], module, name, arity) do + signature_fallback(module, name, arity) + end + + defp format_signatures(signatures, module, _name, _arity) do + signatures_string = Enum.join(signatures, "\n") + module_string = format_signature_module(module) + + module_string <> signatures_string + end + + defp format_type_signature(nil, module, name, arity) do + signature_fallback(module, name, arity) + end + + defp format_type_signature({_type_kind, type}, module, _name, _arity) do + {:"::", _env, [lhs, _rhs]} = Code.Typespec.type_to_quoted(type) + + {name, meta, args} = lhs + + capitalized_args = Enum.map(args, fn + {var_name, var_meta, context} when is_atom(var_name) -> + string_name = Atom.to_string(var_name) + {first, rest} = String.split_at(string_name, 1) + + new_name = + (String.upcase(first) <> rest) + |> String.to_atom() + + {new_name, var_meta, context} + + other -> other + end) + + new_lhs = {name, meta, capitalized_args} + + module_string = format_signature_module(module) + + module_string <> Macro.to_string(new_lhs) + end + + defp signature_fallback(module, name, arity) do + args = Enum.map_join(1..arity//1, ", ", fn n -> "Arg#{n}" end) + "#{module}:#{name}(#{args})" + end + + defp format_signature_module(module) do + if module == :erlang do + "" + else + "#{module}:" + end + end + + defp format_specs([], _name, _arity), do: nil + + defp format_specs(specs, name, arity) do + erl_attribute = {:attribute, 1, :spec, {{name, arity}, specs}} + format_spec(erl_attribute) + + rescue + _ -> nil + end + + defp format_type_spec({type_kind, type}) do + erl_attribute = {:attribute, 1, type_kind, type} + format_spec(erl_attribute) + + rescue + _ -> nil + end + + defp format_type_spec(_), do: nil + + defp format_spec(ast) do + {:attribute, _, type, _} = ast + + offset = byte_size(Atom.to_string(type)) + 2 + + options = [linewidth: 98 + offset] + + :erl_pp.attribute(ast) + |> IO.chardata_to_string() + |> String.trim() + end + @keywords [ {"true", "(boolean)"}, {"false", "(boolean)"}, diff --git a/lib/livebook/intellisense/erlang/identifier_matcher.ex b/lib/livebook/intellisense/erlang/identifier_matcher.ex index 782a12726e3..e71c13b1b9a 100644 --- a/lib/livebook/intellisense/erlang/identifier_matcher.ex +++ b/lib/livebook/intellisense/erlang/identifier_matcher.ex @@ -233,8 +233,8 @@ defmodule Livebook.Intellisense.Erlang.IdentifierMatcher do taken end - defp token_to_text({_, _, val}), do: Atom.to_string(val) - defp token_to_text({val, _}), do: Atom.to_string(val) + defp token_to_text({_, _, val}), do: to_string(val) + defp token_to_text({val, _}), do: to_string(val) defp match_tokens_to_context_with_columns(tokens) do case tokens do From 4ba21cc6b314f738703b19359dfeeabe0c23a85d Mon Sep 17 00:00:00 2001 From: Filip Date: Mon, 16 Feb 2026 15:22:35 +0100 Subject: [PATCH 26/26] fix locate definition for erlang --- lib/livebook/intellisense/elixir/docs.ex | 18 ++++++++---------- lib/livebook/intellisense/erlang.ex | 4 +--- lib/livebook/runtime/evaluator.ex | 2 +- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/lib/livebook/intellisense/elixir/docs.ex b/lib/livebook/intellisense/elixir/docs.ex index 576b59d2db3..938ecb3dcb0 100644 --- a/lib/livebook/intellisense/elixir/docs.ex +++ b/lib/livebook/intellisense/elixir/docs.ex @@ -205,13 +205,11 @@ defmodule Livebook.Intellisense.Elixir.Docs do with {:ok, {:debug_info_v1, _, {:elixir_v1, meta, _}}} <- beam_lib_chunks(path, :debug_info), {_pair, _kind, kw, _body} <- keyfind(meta.definitions, {name, arity}) do Keyword.fetch(kw, :line) - #TODO: maybe we should create separate function flow for erlang, or at least handle case where both fail with :error here else _ -> locate_erlang_function(path, name, arity) end end - #TODO: maybe we should locate types for erlang too, not only functions def locate_definition(path, {:type, name, arity}) do case beam_lib_chunks(path, :abstract_code) do {:ok, {:raw_abstract_v1, annotations}} -> @@ -223,14 +221,14 @@ defmodule Livebook.Intellisense.Elixir.Docs do end defp locate_erlang_function(path, name, arity) do - with {:ok, {:raw_abstract_v1, annotations}} <- beam_lib_chunks(path, :abstract_code) do - result = - Enum.find_value(annotations, fn - {:function, anno, ^name, ^arity, _} -> :erl_anno.line(anno) - _ -> nil - end) - - if result, do: {:ok, result}, else: :error + with {:ok, {:raw_abstract_v1, annotations}} <- + beam_lib_chunks(path, :abstract_code), + line when is_integer(line) <- + Enum.find_value(annotations, fn + {:function, anno, ^name, ^arity, _} -> :erl_anno.line(anno) + _ -> nil + end) do + {:ok, line} else _ -> :error end diff --git a/lib/livebook/intellisense/erlang.ex b/lib/livebook/intellisense/erlang.ex index 244d6c0f0c3..12e7f599aea 100644 --- a/lib/livebook/intellisense/erlang.ex +++ b/lib/livebook/intellisense/erlang.ex @@ -4,8 +4,6 @@ defmodule Livebook.Intellisense.Erlang do @behaviour Intellisense - @line_length 45 - @impl true def handle_request({:format, _code}, _context, _node) do # Not supported. @@ -362,7 +360,7 @@ defmodule Livebook.Intellisense.Erlang do options = [linewidth: 98 + offset] - :erl_pp.attribute(ast) + :erl_pp.attribute(ast, options) |> IO.chardata_to_string() |> String.trim() end diff --git a/lib/livebook/runtime/evaluator.ex b/lib/livebook/runtime/evaluator.ex index 246363044a9..fd97138c974 100644 --- a/lib/livebook/runtime/evaluator.ex +++ b/lib/livebook/runtime/evaluator.ex @@ -751,7 +751,7 @@ defmodule Livebook.Runtime.Evaluator do try do {:ok, forms} = :epp.parse_file(filename, source_name: String.to_charlist(env.file)) - case :compile.forms(forms, [:debug_info]) do + case :compile.forms(forms, [:debug_info, source: String.to_charlist(env.file)]) do {:ok, module, binary} -> file = if ebin_path = ebin_path() do