Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
86565f6
Erlang Intellisense initial module item completion
kxlsx Nov 21, 2025
6c8bcbe
Added Variable completion.
kxlsx Nov 22, 2025
5572d0d
Case statement instead of signature pattern match
oakrotka Dec 4, 2025
8ded1c5
Merge exported functions into one pipeline
oakrotka Dec 4, 2025
eefabae
Merge pull request #1 from kxlsx/erlang-module-completion-ldr
kxlsx Dec 4, 2025
7c1dfd3
first working version of docs for modules and functions
wegorz13 Dec 9, 2025
2e8359b
add docs for locate_identifier()
wegorz13 Dec 27, 2025
8f7c13d
completion of erlang modules in erlang
oakrotka Jan 5, 2026
43b225d
hardcoded module attributes without docs
AdrianGlanowski Jan 6, 2026
c7fcf16
styled way to complete erlang module attributes
AdrianGlanowski Jan 6, 2026
f4e621a
move erlang variable completion to a function
oakrotka Jan 7, 2026
e98e826
keyword and operator completion in erlang
oakrotka Jan 7, 2026
917cf6a
bitstring modifier completion in erlang
oakrotka Jan 8, 2026
4409e87
add BIF function and type completion and docs, as well as types in ge…
wegorz13 Jan 9, 2026
250f1f2
add docs for erlang -docs module attribute
wegorz13 Jan 9, 2026
743c8c3
fix for no record completion
wegorz13 Jan 15, 2026
7b5ae6a
Erlang Intellisense signature info
kxlsx Jan 17, 2026
88b38d1
Erlang Intellisense Signature Matching - Fixed BIF completion
kxlsx Jan 17, 2026
f69f84a
Merge pull request #2 from kxlsx/erlang-docs
kxlsx Jan 17, 2026
f06c939
Merge branch 'erlang-intellisense' into erlang-macros-and-directives-…
kxlsx Jan 17, 2026
b26c2d6
Merge pull request #4 from kxlsx/erlang-macros-and-directives-completion
kxlsx Jan 17, 2026
79089c8
Merge branch 'erlang-intellisense' into erlang-completion-oza
kxlsx Jan 17, 2026
4d95462
Merge pull request #3 from kxlsx/erlang-completion-oza
kxlsx Jan 17, 2026
990d14b
Merge pull request #5 from kxlsx/erlang-signature
kxlsx Jan 17, 2026
3681b71
Fixed merge errors - missing 'end' clause
kxlsx Jan 17, 2026
b07f33e
remove commented code
wegorz13 Jan 17, 2026
c3c1cbf
remove debug prints and obsolete TODOs
oakrotka Jan 17, 2026
e6553bb
fix unused variable warnings
oakrotka Jan 17, 2026
add29c7
fix function clause grouping error
oakrotka Jan 17, 2026
f20bef2
fix formatting
oakrotka Jan 17, 2026
e23420e
Merge pull request #6 from kxlsx/small-fixes
oakrotka Jan 17, 2026
d824633
Merge branch 'livebook-dev:main' into erlang-intellisense
kxlsx Jan 19, 2026
9ca8cf7
Merge branch 'erlang-intellisense' into erlang-docs
wegorz13 Feb 14, 2026
55850d2
Compile Erlang modules with debug_info
jonatanklosko Feb 11, 2026
953c6f1
change signature and spec formatting to erlang style
wegorz13 Feb 15, 2026
4ba21cc
fix locate definition for erlang
wegorz13 Feb 16, 2026
0fa227c
Merge pull request #7 from kxlsx/erlang-docs
wegorz13 Feb 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 77 additions & 40 deletions lib/livebook/intellisense/elixir.ex
Original file line number Diff line number Diff line change
Expand Up @@ -40,19 +40,24 @@ 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

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: %{
Expand Down Expand Up @@ -133,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,
Expand Down Expand Up @@ -192,14 +198,41 @@ 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 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:
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),
Expand All @@ -212,6 +245,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
Expand All @@ -228,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
Expand Down Expand Up @@ -256,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

Expand Down Expand Up @@ -309,15 +343,15 @@ 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
{completion_item_kind_priority(completion_item.kind), completion_item.label}
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

Expand All @@ -338,22 +372,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),
"""
Expand All @@ -366,15 +399,17 @@ defmodule Livebook.Intellisense.Elixir do
])
end

defp format_details_item(%{kind: :module, module: module, documentation: documentation}) do
#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)),
format_docs_link(module),
Intellisense.Elixir.Docs.format_documentation(documentation, :all)
])
end

defp format_details_item(%{
#TODO: format_signatures and format_specs needs to be reworked for erlang functions
def format_details_item(%{
kind: :function,
module: module,
name: name,
Expand All @@ -396,7 +431,7 @@ defmodule Livebook.Intellisense.Elixir do
])
end

defp format_details_item(%{
def format_details_item(%{
kind: :type,
module: module,
name: name,
Expand All @@ -412,38 +447,38 @@ 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")

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
Expand All @@ -470,7 +505,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)
Expand All @@ -485,30 +521,30 @@ 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}
```\
"""
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)

Expand Down Expand Up @@ -558,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")

Expand All @@ -583,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

Expand Down
36 changes: 30 additions & 6 deletions lib/livebook/intellisense/elixir/docs.ex
Original file line number Diff line number Diff line change
Expand Up @@ -189,24 +189,48 @@ 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
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)
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

defp locate_erlang_function(path, name, arity) do
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
end

Expand Down
Loading