Add per-language dispatch to intellisense

This commit is contained in:
Jonatan Kłosko 2025-11-14 21:45:52 +01:00
parent 491e75f34f
commit fb310d439c
27 changed files with 3589 additions and 3047 deletions

View file

@ -506,7 +506,9 @@ export default class LiveEditor {
const settings = settingsStore.get();
// Trigger completion implicitly only for identifiers and members
const triggerBeforeCursor = context.matchBefore(/[\w?!.]$/);
const triggerBeforeCursor = context.matchBefore(
this.getTriggerBeforeCursorRegex(),
);
if (!triggerBeforeCursor && !context.explicit) {
return null;
@ -546,6 +548,13 @@ export default class LiveEditor {
.catch(() => null);
}
/** Get the regex for the trigger before cursor */
getTriggerBeforeCursorRegex() {
if (this.language === "elixir") return /[\w?!.]$/;
if (this.language === "erlang") return /[\w:]$/;
return /[\w.]$/;
}
/** @private */
getCompletionHint(context) {
// By default we only send the current line content until cursor

View file

@ -1,17 +1,12 @@
defmodule Livebook.Intellisense do
# This module provides intellisense related operations suitable for
# integration with a text editor.
# Language-specific intellisense that is used by the code editor.
#
# In a way, this provides the very basic features of a language
# server that Livebook uses.
# This module defines a behaviour and dispatches the intellisense
# implementation to the appropriate language-specific module.
alias Livebook.Intellisense
alias Livebook.Runtime
# Configures width used for inspect and specs formatting.
@line_length 45
@extended_line_length 80
@typedoc """
Evaluation state to consider for intellisense.
@ -24,6 +19,35 @@ defmodule Livebook.Intellisense do
map_binding: (Code.binding() -> any())
}
@doc """
Language-specific implementation of `t:Runtime.intellisense_request/1`.
"""
@callback handle_request(
request :: Runtime.intellisense_request(),
context :: context(),
node :: node()
) :: Runtime.intellisense_response()
@doc """
Resolves an intellisense request as defined in
`t:Runtime.intellisense_request/1`.
"""
@spec handle_request(
Runtime.language(),
Runtime.intellisense_request(),
context(),
node()
) :: Runtime.intellisense_response()
def handle_request(language, request, context, node) do
if impl = impl_for_language(language) do
impl.handle_request(request, context, node)
end
end
defp impl_for_language(:elixir), do: Intellisense.Elixir
defp impl_for_language(:erlang), do: Intellisense.Erlang
defp impl_for_language(_other), do: nil
@doc """
Adjusts the system for more accurate intellisense.
"""
@ -57,896 +81,6 @@ defmodule Livebook.Intellisense do
"""
@spec clear_cache(node()) :: :ok
def clear_cache(node) do
Intellisense.IdentifierMatcher.clear_all_loaded(node)
end
@doc """
Resolves an intellisense request as defined by `Runtime`.
In practice this function simply dispatches the request to one of
the other public functions in this module.
"""
@spec handle_request(
Runtime.intellisense_request(),
context(),
node()
) :: Runtime.intellisense_response()
def handle_request(request, context, node)
def handle_request({:completion, hint}, context, node) do
items = get_completion_items(hint, context, node)
%{items: items}
end
def handle_request({:details, line, column}, context, node) do
get_details(line, column, context, node)
end
def handle_request({:signature, hint}, context, node) do
get_signature_items(hint, context, node)
end
def handle_request({:format, code}, _context, _node) do
format_code(code)
end
@doc """
Formats Elixir code.
"""
@spec format_code(String.t()) :: Runtime.format_response()
def format_code(code) do
try do
formatted =
code
|> Code.format_string!()
|> IO.iodata_to_binary()
%{code: formatted, code_markers: []}
rescue
error in [SyntaxError, TokenMissingError, MismatchedDelimiterError] ->
code_marker = %{line: error.line, description: error.description, severity: :error}
%{code: nil, code_markers: [code_marker]}
end
end
@doc """
Returns information about signatures matching the given `hint`.
"""
@spec get_signature_items(String.t(), context(), node()) :: Runtime.signature_response() | nil
def get_signature_items(hint, context, node) do
case Intellisense.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(&format_signature_item/1)
|> Enum.uniq()
}
:error ->
nil
end
end
defp format_signature_item({_name, signature, _documentation, _specs}),
do: %{
signature: signature,
arguments: arguments_from_signature(signature)
}
defp arguments_from_signature(signature) do
signature
|> Code.string_to_quoted!()
|> elem(2)
|> Enum.map(&Macro.to_string/1)
end
@doc """
Returns a list of completion suggestions for the given `hint`.
"""
@spec get_completion_items(String.t(), context(), node()) :: list(Runtime.completion_item())
def get_completion_items(hint, context, node) do
Intellisense.IdentifierMatcher.completion_identifiers(hint, context, node)
|> Enum.filter(&include_in_completion?/1)
|> Enum.map(&format_completion_item/1)
|> Enum.concat(extra_completion_items(hint))
|> Enum.sort_by(&completion_item_priority/1)
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
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: :map_field, name: name}),
do: %{
label: Atom.to_string(name),
kind: :field,
documentation: "(field)",
insert_text: Atom.to_string(name)
}
defp format_completion_item(%{kind: :in_map_field, name: name}),
do: %{
label: Atom.to_string(name),
kind: :field,
documentation: "(field)",
insert_text: "#{name}: "
}
defp format_completion_item(%{
kind: :in_struct_field,
struct: struct,
name: name,
default: default
}),
do: %{
label: Atom.to_string(name),
kind: :field,
documentation:
join_with_divider([
"""
`%#{inspect(struct)}{}` struct field.
**Default**
```
#{inspect(default, pretty: true, width: @line_length)}
```\
"""
]),
insert_text: "#{name}: "
}
defp format_completion_item(%{
kind: :module,
module: module,
display_name: display_name,
documentation: documentation
}) do
subtype = Intellisense.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:
join_with_newlines([
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:
join_with_newlines([
format_documentation(documentation, :short),
code(format_signatures(signatures, module, name, arity))
]),
insert_text:
cond do
type == :macro and keyword_macro?(name) ->
"#{display_name} "
type == :macro and 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:
join_with_newlines([
format_documentation(documentation, :short),
format_type_spec(type_spec, @line_length) |> code()
]),
insert_text:
cond do
arity == 0 -> "#{Atom.to_string(name)}()"
true -> "#{Atom.to_string(name)}(${})"
end
}
defp format_completion_item(%{
kind: :module_attribute,
name: name,
documentation: documentation
}),
do: %{
label: Atom.to_string(name),
kind: :variable,
documentation:
join_with_newlines([
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 keyword_macro?(name) do
def? = name |> Atom.to_string() |> String.starts_with?("def")
def? or
name in [
# Special forms
:alias,
:case,
:cond,
:for,
:fn,
:import,
:quote,
:receive,
:require,
:try,
:with,
# Kernel
:destructure,
:raise,
:reraise,
:if,
:unless,
:use
]
end
defp env_macro?(name) do
name in [:__ENV__, :__MODULE__, :__DIR__, :__STACKTRACE__, :__CALLER__]
end
defp extra_completion_items(hint) do
items = [
%{
label: "true",
kind: :keyword,
documentation: "(boolean)",
insert_text: "true"
},
%{
label: "false",
kind: :keyword,
documentation: "(boolean)",
insert_text: "false"
},
%{
label: "nil",
kind: :keyword,
documentation: "(special atom)",
insert_text: "nil"
},
%{
label: "when",
kind: :keyword,
documentation: "(guard operator)",
insert_text: "when"
}
]
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
@ordered_kinds [
:keyword,
:field,
:variable,
:module,
:struct,
:interface,
:function,
:type,
:bitstring_option
]
defp 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
{completion_item_kind_priority(completion_item.kind), completion_item.label}
end
defp completion_item_kind_priority(kind) when kind in @ordered_kinds do
Enum.find_index(@ordered_kinds, &(&1 == kind))
end
@doc """
Returns detailed information about an identifier located
in `column` in `line`.
"""
@spec get_details(String.t(), pos_integer(), context(), node()) ::
Runtime.details_response() | nil
def get_details(line, column, context, node) do
%{matches: matches, range: range} =
Intellisense.IdentifierMatcher.locate_identifier(line, column, context, node)
case Enum.filter(matches, &include_in_details?/1) do
[] ->
nil
matches ->
matches = Enum.sort_by(matches, & &1[:arity], :asc)
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
defp format_details_item(%{kind: :variable, name: name}), do: code(name)
defp format_details_item(%{kind: :map_field, name: name}), do: code(name)
defp 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
join_with_divider([
code(name),
"""
**Default**
```
#{inspect(default, pretty: true, width: @line_length)}
```\
"""
])
end
defp format_details_item(%{kind: :module, module: module, documentation: documentation}) do
join_with_divider([
code(inspect(module)),
format_docs_link(module),
format_documentation(documentation, :all)
])
end
defp format_details_item(%{
kind: :function,
module: module,
name: name,
arity: arity,
documentation: documentation,
signatures: signatures,
specs: specs,
meta: meta
}) do
join_with_divider([
format_signatures(signatures, module, name, arity) |> code(),
join_with_middle_dot([
format_docs_link(module, {:function, name, arity}),
format_meta(:since, meta)
]),
format_meta(:deprecated, meta),
format_specs(specs, name, @extended_line_length) |> code(),
format_documentation(documentation, :all)
])
end
defp format_details_item(%{
kind: :type,
module: module,
name: name,
arity: arity,
documentation: documentation,
type_spec: type_spec
}) do
join_with_divider([
format_type_signature(type_spec, module, name, arity) |> code(),
format_docs_link(module, {:type, name, arity}),
format_type_spec(type_spec, @extended_line_length) |> code(),
format_documentation(documentation, :all)
])
end
defp format_details_item(%{kind: :module_attribute, name: name, documentation: documentation}) do
join_with_divider([
code("@#{name}"),
format_documentation(documentation, :all)
])
end
defp get_definition_location(%{kind: :module, module: module}, context) do
get_definition_location(module, context, {:module, module})
end
defp 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
get_definition_location(module, context, {:type, name, arity})
end
defp get_definition_location(_idenfitier, _context), do: nil
defp 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.Docs.locate_definition(String.to_charlist(path), identifier) do
file = module.module_info(:compile)[:source]
%{file: to_string(file), line: line}
else
_otherwise -> nil
end
end
end
# Formatting helpers
defp join_with_divider(strings), do: join_with(strings, "\n\n---\n\n")
defp join_with_newlines(strings), do: join_with(strings, "\n\n")
defp join_with_middle_dot(strings), do: join_with(strings, " · ")
defp 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
defp code(code) do
"""
```
#{code}
```\
"""
end
defp format_docs_link(module, function_or_type \\ nil) do
app = Application.get_application(module)
module_name = module_name(module)
is_otp? =
case :code.which(module) do
:preloaded -> true
[_ | _] = path -> List.starts_with?(path, :code.lib_dir())
_ -> false
end
cond do
is_otp? ->
hash =
case function_or_type do
{:function, function, arity} -> "##{function}-#{arity}"
{:type, type, _arity} -> "#type-#{type}"
nil -> ""
end
url = "https://www.erlang.org/doc/man/#{module_name}.html#{hash}"
"[View on Erlang Docs](#{url})"
vsn = app && Application.spec(app, :vsn) ->
hash =
case function_or_type do
{:function, function, arity} -> "##{function}/#{arity}"
{:type, type, arity} -> "#t:#{type}/#{arity}"
nil -> ""
end
url = "https://hexdocs.pm/#{app}/#{vsn}/#{module_name}.html#{hash}"
"[View on Hexdocs](#{url})"
true ->
nil
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")
# Don't add module prefix to operator signatures
if :binary.match(signatures_string, ["(", "/"]) != :nomatch do
inspect(module) <> "." <> signatures_string
else
signatures_string
end
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)
inspect(module) <> "." <> Macro.to_string(lhs)
end
defp signature_fallback(module, name, arity) do
args = Enum.map_join(1..arity//1, ", ", fn n -> "arg#{n}" end)
"#{inspect(module)}.#{name}(#{args})"
end
defp format_meta(:deprecated, %{deprecated: deprecated}) do
"**Deprecated**. " <> deprecated
end
defp format_meta(:since, %{since: since}) do
"Since " <> since
end
defp format_meta(_, _), do: nil
defp format_specs([], _name, _line_length), do: nil
defp format_specs(specs, name, line_length) do
spec_lines =
Enum.map(specs, fn spec ->
code = Code.Typespec.spec_to_quoted(name, spec) |> Macro.to_string()
["@spec ", code]
end)
specs_code =
spec_lines
|> Enum.intersperse("\n")
|> IO.iodata_to_binary()
try do
Code.format_string!(specs_code, line_length: line_length)
rescue
_ -> specs_code
end
end
defp format_type_spec({type_kind, type}, line_length) when type_kind in [:type, :opaque] do
ast = {:"::", _env, [lhs, _rhs]} = Code.Typespec.type_to_quoted(type)
type_string =
case type_kind do
:type -> ast
:opaque -> lhs
end
|> Macro.to_string()
type_spec_code = "@#{type_kind} #{type_string}"
try do
Code.format_string!(type_spec_code, line_length: line_length)
rescue
_ -> type_spec_code
end
end
defp format_type_spec(_, _line_length), do: nil
defp format_documentation(doc, variant)
defp format_documentation(nil, _variant) do
"No documentation available"
end
defp format_documentation(:hidden, _variant) do
"This is a private API"
end
defp format_documentation({"text/markdown", markdown}, :short) do
# Extract just the first paragraph
markdown
|> String.split("\n\n")
|> hd()
|> String.trim()
end
defp format_documentation({"application/erlang+html", erlang_html}, :short) do
# Extract just the first paragraph
erlang_html
|> Enum.find(&match?({:p, _, _}, &1))
|> case do
nil -> nil
paragraph -> erlang_html_to_md([paragraph])
end
end
defp format_documentation({"text/markdown", markdown}, :all) do
markdown
end
defp format_documentation({"application/erlang+html", erlang_html}, :all) do
erlang_html_to_md(erlang_html)
end
defp format_documentation({format, _content}, _variant) do
raise "unknown documentation format #{inspect(format)}"
end
# Erlang HTML AST
# See https://erlang.org/doc/apps/erl_docgen/doc_storage.html#erlang-documentation-format
def erlang_html_to_md(ast) do
build_md([], ast)
|> IO.iodata_to_binary()
|> String.trim()
end
defp build_md(iodata, ast)
defp build_md(iodata, []), do: iodata
defp build_md(iodata, [string | ast]) when is_binary(string) do
string |> append_inline(iodata) |> build_md(ast)
end
defp build_md(iodata, [{tag, _, content} | ast]) when tag in [:em, :i] do
render_emphasis(content) |> append_inline(iodata) |> build_md(ast)
end
defp build_md(iodata, [{tag, _, content} | ast]) when tag in [:strong, :b] do
render_strong(content) |> append_inline(iodata) |> build_md(ast)
end
defp build_md(iodata, [{:code, _, content} | ast]) do
render_code_inline(content) |> append_inline(iodata) |> build_md(ast)
end
defp build_md(iodata, [{:a, attrs, content} | ast]) do
render_link(content, attrs) |> append_inline(iodata) |> build_md(ast)
end
defp build_md(iodata, [{:br, _, []} | ast]) do
render_line_break() |> append_inline(iodata) |> build_md(ast)
end
defp build_md(iodata, [{tag, _, content} | ast]) when tag in [:p, :div] do
render_paragraph(content) |> append_block(iodata) |> build_md(ast)
end
@headings ~w(h1 h2 h3 h4 h5 h6)a
defp build_md(iodata, [{tag, _, content} | ast]) when tag in @headings do
n = 1 + Enum.find_index(@headings, &(&1 == tag))
render_heading(n, content) |> append_block(iodata) |> build_md(ast)
end
defp build_md(iodata, [{:pre, _, [{:code, _, [content]}]} | ast]) do
render_code_block(content, "erlang") |> append_block(iodata) |> build_md(ast)
end
defp build_md(iodata, [{:div, [{:class, class} | _], content} | ast]) do
type = class |> to_string() |> String.upcase()
render_blockquote([{:p, [], [{:strong, [], [type]}]} | content])
|> append_block(iodata)
|> build_md(ast)
end
defp build_md(iodata, [{:ul, [{:class, "types"} | _], content} | ast]) do
render_types_list(content) |> append_block(iodata) |> build_md(ast)
end
defp build_md(iodata, [{:ul, _, content} | ast]) do
render_unordered_list(content) |> append_block(iodata) |> build_md(ast)
end
defp build_md(iodata, [{:ol, _, content} | ast]) do
render_ordered_list(content) |> append_block(iodata) |> build_md(ast)
end
defp build_md(iodata, [{:dl, _, content} | ast]) do
render_description_list(content) |> append_block(iodata) |> build_md(ast)
end
defp append_inline(md, iodata), do: [iodata, md]
defp append_block(md, iodata), do: [iodata, "\n", md, "\n"]
# Renderers
defp render_emphasis(content) do
["*", build_md([], content), "*"]
end
defp render_strong(content) do
["**", build_md([], content), "**"]
end
defp render_code_inline(content) do
["`", build_md([], content), "`"]
end
defp render_link(content, attrs) do
caption = build_md([], content)
if href = attrs[:href] do
["[", caption, "](", href, ")"]
else
caption
end
end
defp render_line_break(), do: "\\\n"
defp render_paragraph(content), do: erlang_html_to_md(content)
defp render_heading(n, content) do
title = build_md([], content)
[String.duplicate("#", n), " ", title]
end
defp render_code_block(content, language) do
["```", language, "\n", content, "\n```"]
end
defp render_blockquote(content) do
inner = erlang_html_to_md(content)
inner
|> String.split("\n")
|> Enum.map_intersperse("\n", &["> ", &1])
end
defp render_unordered_list(content) do
marker_fun = fn _index -> "* " end
render_list(content, marker_fun, " ")
end
defp render_ordered_list(content) do
marker_fun = fn index -> "#{index + 1}. " end
render_list(content, marker_fun, " ")
end
defp render_list(items, marker_fun, indent) do
spaced? = spaced_list_items?(items)
item_separator = if(spaced?, do: "\n\n", else: "\n")
items
|> Enum.map(fn {:li, _, content} -> erlang_html_to_md(content) end)
|> Enum.with_index()
|> Enum.map(fn {inner, index} ->
[first_line | lines] = String.split(inner, "\n")
first_line = marker_fun.(index) <> first_line
lines =
Enum.map(lines, fn
"" -> ""
line -> indent <> line
end)
Enum.intersperse([first_line | lines], "\n")
end)
|> Enum.intersperse(item_separator)
end
defp spaced_list_items?([{:li, _, [{:p, _, _content} | _]} | _items]), do: true
defp spaced_list_items?([_ | items]), do: spaced_list_items?(items)
defp spaced_list_items?([]), do: false
defp render_description_list(content) do
# Rewrite description list as an unordered list with pseudo heading
content
|> Enum.chunk_every(2)
|> Enum.map(fn [{:dt, _, dt}, {:dd, _, dd}] ->
{:li, [], [{:p, [], [{:strong, [], dt}]}, {:p, [], dd}]}
end)
|> render_unordered_list()
end
defp render_types_list(content) do
content
|> group_type_list_items([])
|> render_unordered_list()
end
defp group_type_list_items([], acc), do: Enum.reverse(acc)
defp group_type_list_items([{:li, [{:name, _type_name}], []} | items], acc) do
group_type_list_items(items, acc)
end
defp group_type_list_items([{:li, [{:class, "type"}], content} | items], acc) do
group_type_list_items(items, [{:li, [], [{:code, [], content}]} | acc])
end
defp group_type_list_items(
[{:li, [{:class, "description"}], content} | items],
[{:li, [], prev_content} | acc]
) do
group_type_list_items(items, [{:li, [], prev_content ++ [{:p, [], content}]} | acc])
end
defp module_name(module) do
case Atom.to_string(module) do
"Elixir." <> name -> name
name -> name
end
Intellisense.Elixir.IdentifierMatcher.clear_all_loaded(node)
end
end

View file

@ -0,0 +1,637 @@
defmodule Livebook.Intellisense.Elixir do
alias Livebook.Intellisense
@behaviour Intellisense
# Configures width used for inspect and specs formatting.
@line_length 45
@extended_line_length 80
@impl true
def handle_request({:format, code}, _context, _node) do
handle_format(code)
end
def handle_request({:completion, hint}, context, node) do
handle_completion(hint, context, node)
end
def handle_request({:details, line, column}, context, node) do
handle_details(line, column, context, node)
end
def handle_request({:signature, hint}, context, node) do
handle_signature(hint, context, node)
end
defp handle_format(code) do
try do
formatted =
code
|> Code.format_string!()
|> IO.iodata_to_binary()
%{code: formatted, code_markers: []}
rescue
error in [SyntaxError, TokenMissingError, MismatchedDelimiterError] ->
code_marker = %{line: error.line, description: error.description, severity: :error}
%{code: nil, code_markers: [code_marker]}
end
end
defp handle_completion(hint, context, node) do
items =
Intellisense.Elixir.IdentifierMatcher.completion_identifiers(hint, context, node)
|> Enum.filter(&include_in_completion?/1)
|> Enum.map(&format_completion_item/1)
|> Enum.concat(extra_completion_items(hint))
|> 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
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: :map_field, name: name}),
do: %{
label: Atom.to_string(name),
kind: :field,
documentation: "(field)",
insert_text: Atom.to_string(name)
}
defp format_completion_item(%{kind: :in_map_field, name: name}),
do: %{
label: Atom.to_string(name),
kind: :field,
documentation: "(field)",
insert_text: "#{name}: "
}
defp format_completion_item(%{
kind: :in_struct_field,
struct: struct,
name: name,
default: default
}),
do: %{
label: Atom.to_string(name),
kind: :field,
documentation:
join_with_divider([
"""
`%#{inspect(struct)}{}` struct field.
**Default**
```
#{inspect(default, pretty: true, width: @line_length)}
```\
"""
]),
insert_text: "#{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:
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:
join_with_newlines([
Intellisense.Elixir.Docs.format_documentation(documentation, :short),
code(format_signatures(signatures, module, name, arity))
]),
insert_text:
cond do
type == :macro and keyword_macro?(name) ->
"#{display_name} "
type == :macro and 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:
join_with_newlines([
Intellisense.Elixir.Docs.format_documentation(documentation, :short),
format_type_spec(type_spec, @line_length) |> code()
]),
insert_text:
cond do
arity == 0 -> "#{Atom.to_string(name)}()"
true -> "#{Atom.to_string(name)}(${})"
end
}
defp format_completion_item(%{
kind: :module_attribute,
name: name,
documentation: documentation
}),
do: %{
label: Atom.to_string(name),
kind: :variable,
documentation:
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 keyword_macro?(name) do
def? = name |> Atom.to_string() |> String.starts_with?("def")
def? or
name in [
# Special forms
:alias,
:case,
:cond,
:for,
:fn,
:import,
:quote,
:receive,
:require,
:try,
:with,
# Kernel
:destructure,
:raise,
:reraise,
:if,
:unless,
:use
]
end
defp env_macro?(name) do
name in [:__ENV__, :__MODULE__, :__DIR__, :__STACKTRACE__, :__CALLER__]
end
defp extra_completion_items(hint) do
items = [
%{
label: "true",
kind: :keyword,
documentation: "(boolean)",
insert_text: "true"
},
%{
label: "false",
kind: :keyword,
documentation: "(boolean)",
insert_text: "false"
},
%{
label: "nil",
kind: :keyword,
documentation: "(special atom)",
insert_text: "nil"
},
%{
label: "when",
kind: :keyword,
documentation: "(guard operator)",
insert_text: "when"
}
]
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
@ordered_kinds [
:keyword,
:field,
:variable,
:module,
:struct,
:interface,
:function,
:type,
:bitstring_option
]
defp 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
{completion_item_kind_priority(completion_item.kind), completion_item.label}
end
defp completion_item_kind_priority(kind) when kind in @ordered_kinds do
Enum.find_index(@ordered_kinds, &(&1 == kind))
end
defp handle_details(line, column, context, node) do
%{matches: matches, range: range} =
Intellisense.Elixir.IdentifierMatcher.locate_identifier(line, column, context, node)
case Enum.filter(matches, &include_in_details?/1) do
[] ->
nil
matches ->
matches = Enum.sort_by(matches, & &1[:arity], :asc)
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
defp format_details_item(%{kind: :variable, name: name}), do: code(name)
defp format_details_item(%{kind: :map_field, name: name}), do: code(name)
defp 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
join_with_divider([
code(name),
"""
**Default**
```
#{inspect(default, pretty: true, width: @line_length)}
```\
"""
])
end
defp 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(%{
kind: :function,
module: module,
name: name,
arity: arity,
documentation: documentation,
signatures: signatures,
specs: specs,
meta: meta
}) do
join_with_divider([
format_signatures(signatures, module, name, arity) |> code(),
join_with_middle_dot([
format_docs_link(module, {:function, name, arity}),
format_meta(:since, meta)
]),
format_meta(:deprecated, meta),
format_specs(specs, name, @extended_line_length) |> code(),
Intellisense.Elixir.Docs.format_documentation(documentation, :all)
])
end
defp format_details_item(%{
kind: :type,
module: module,
name: name,
arity: arity,
documentation: documentation,
type_spec: type_spec
}) do
join_with_divider([
format_type_signature(type_spec, module, name, arity) |> code(),
format_docs_link(module, {:type, name, arity}),
format_type_spec(type_spec, @extended_line_length) |> code(),
Intellisense.Elixir.Docs.format_documentation(documentation, :all)
])
end
defp 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
get_definition_location(module, context, {:module, module})
end
defp 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
get_definition_location(module, context, {:type, name, arity})
end
defp get_definition_location(_idenfitier, _context), do: nil
defp 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]
%{file: to_string(file), line: line}
else
_otherwise -> nil
end
end
end
defp handle_signature(hint, context, node) do
case Intellisense.Elixir.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(&format_signature_item/1)
|> Enum.uniq()
}
:error ->
nil
end
end
defp format_signature_item({_name, signature, _documentation, _specs}),
do: %{
signature: signature,
arguments: arguments_from_signature(signature)
}
defp arguments_from_signature(signature) do
signature
|> Code.string_to_quoted!()
|> elem(2)
|> Enum.map(&Macro.to_string/1)
end
# Formatting helpers
defp join_with_divider(strings), do: join_with(strings, "\n\n---\n\n")
defp join_with_newlines(strings), do: join_with(strings, "\n\n")
defp join_with_middle_dot(strings), do: join_with(strings, " · ")
defp 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
defp code(code) do
"""
```
#{code}
```\
"""
end
defp format_docs_link(module, function_or_type \\ nil) do
app = Application.get_application(module)
module_name = module_name(module)
is_otp? =
case :code.which(module) do
:preloaded -> true
[_ | _] = path -> List.starts_with?(path, :code.lib_dir())
_ -> false
end
cond do
is_otp? ->
hash =
case function_or_type do
{:function, function, arity} -> "##{function}-#{arity}"
{:type, type, _arity} -> "#type-#{type}"
nil -> ""
end
url = "https://www.erlang.org/doc/man/#{module_name}.html#{hash}"
"[View on Erlang Docs](#{url})"
vsn = app && Application.spec(app, :vsn) ->
hash =
case function_or_type do
{:function, function, arity} -> "##{function}/#{arity}"
{:type, type, arity} -> "#t:#{type}/#{arity}"
nil -> ""
end
url = "https://hexdocs.pm/#{app}/#{vsn}/#{module_name}.html#{hash}"
"[View on Hexdocs](#{url})"
true ->
nil
end
end
defp module_name(module) do
case Atom.to_string(module) do
"Elixir." <> name -> name
name -> name
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")
# Don't add module prefix to operator signatures
if :binary.match(signatures_string, ["(", "/"]) != :nomatch do
inspect(module) <> "." <> signatures_string
else
signatures_string
end
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)
inspect(module) <> "." <> Macro.to_string(lhs)
end
defp signature_fallback(module, name, arity) do
args = Enum.map_join(1..arity//1, ", ", fn n -> "arg#{n}" end)
"#{inspect(module)}.#{name}(#{args})"
end
defp format_meta(:deprecated, %{deprecated: deprecated}) do
"**Deprecated**. " <> deprecated
end
defp format_meta(:since, %{since: since}) do
"Since " <> since
end
defp format_meta(_, _), do: nil
defp format_specs([], _name, _line_length), do: nil
defp format_specs(specs, name, line_length) do
spec_lines =
Enum.map(specs, fn spec ->
code = Code.Typespec.spec_to_quoted(name, spec) |> Macro.to_string()
["@spec ", code]
end)
specs_code =
spec_lines
|> Enum.intersperse("\n")
|> IO.iodata_to_binary()
try do
Code.format_string!(specs_code, line_length: line_length)
rescue
_ -> specs_code
end
end
defp format_type_spec({type_kind, type}, line_length) when type_kind in [:type, :opaque] do
ast = {:"::", _env, [lhs, _rhs]} = Code.Typespec.type_to_quoted(type)
type_string =
case type_kind do
:type -> ast
:opaque -> lhs
end
|> Macro.to_string()
type_spec_code = "@#{type_kind} #{type_string}"
try do
Code.format_string!(type_spec_code, line_length: line_length)
rescue
_ -> type_spec_code
end
end
defp format_type_spec(_, _line_length), do: nil
end

View file

@ -1,4 +1,4 @@
defmodule Livebook.Intellisense.Docs do
defmodule Livebook.Intellisense.Elixir.Docs do
# This module is responsible for extracting and normalizing
# information like documentation, signatures and specs.
#
@ -230,4 +230,245 @@ defmodule Livebook.Intellisense.Docs do
defp keyfind(list, key) do
List.keyfind(list, key, 0) || :error
end
@doc """
Formats the given documentation content as Markdown.
The `variant` argument can be either `:all` to return the full content,
or `:short` to return only the first paragraph.
"""
@spec format_documentation(documentation(), :all | :short) :: String.t()
def format_documentation(doc, variant)
def format_documentation(nil, _variant) do
"No documentation available"
end
def format_documentation(:hidden, _variant) do
"This is a private API"
end
def format_documentation({"text/markdown", markdown}, :short) do
# Extract just the first paragraph
markdown
|> String.split("\n\n")
|> hd()
|> String.trim()
end
def format_documentation({"application/erlang+html", erlang_html}, :short) do
# Extract just the first paragraph
erlang_html
|> Enum.find(&match?({:p, _, _}, &1))
|> case do
nil -> nil
paragraph -> erlang_html_to_md([paragraph])
end
end
def format_documentation({"text/markdown", markdown}, :all) do
markdown
end
def format_documentation({"application/erlang+html", erlang_html}, :all) do
erlang_html_to_md(erlang_html)
end
def format_documentation({format, _content}, _variant) do
raise "unknown documentation format #{inspect(format)}"
end
# Erlang HTML AST
# See https://erlang.org/doc/apps/erl_docgen/doc_storage.html#erlang-documentation-format
defp erlang_html_to_md(ast) do
build_md([], ast)
|> IO.iodata_to_binary()
|> String.trim()
end
defp build_md(iodata, ast)
defp build_md(iodata, []), do: iodata
defp build_md(iodata, [string | ast]) when is_binary(string) do
string |> append_inline(iodata) |> build_md(ast)
end
defp build_md(iodata, [{tag, _, content} | ast]) when tag in [:em, :i] do
render_emphasis(content) |> append_inline(iodata) |> build_md(ast)
end
defp build_md(iodata, [{tag, _, content} | ast]) when tag in [:strong, :b] do
render_strong(content) |> append_inline(iodata) |> build_md(ast)
end
defp build_md(iodata, [{:code, _, content} | ast]) do
render_code_inline(content) |> append_inline(iodata) |> build_md(ast)
end
defp build_md(iodata, [{:a, attrs, content} | ast]) do
render_link(content, attrs) |> append_inline(iodata) |> build_md(ast)
end
defp build_md(iodata, [{:br, _, []} | ast]) do
render_line_break() |> append_inline(iodata) |> build_md(ast)
end
defp build_md(iodata, [{tag, _, content} | ast]) when tag in [:p, :div] do
render_paragraph(content) |> append_block(iodata) |> build_md(ast)
end
@headings ~w(h1 h2 h3 h4 h5 h6)a
defp build_md(iodata, [{tag, _, content} | ast]) when tag in @headings do
n = 1 + Enum.find_index(@headings, &(&1 == tag))
render_heading(n, content) |> append_block(iodata) |> build_md(ast)
end
defp build_md(iodata, [{:pre, _, [{:code, _, [content]}]} | ast]) do
render_code_block(content, "erlang") |> append_block(iodata) |> build_md(ast)
end
defp build_md(iodata, [{:div, [{:class, class} | _], content} | ast]) do
type = class |> to_string() |> String.upcase()
render_blockquote([{:p, [], [{:strong, [], [type]}]} | content])
|> append_block(iodata)
|> build_md(ast)
end
defp build_md(iodata, [{:ul, [{:class, "types"} | _], content} | ast]) do
render_types_list(content) |> append_block(iodata) |> build_md(ast)
end
defp build_md(iodata, [{:ul, _, content} | ast]) do
render_unordered_list(content) |> append_block(iodata) |> build_md(ast)
end
defp build_md(iodata, [{:ol, _, content} | ast]) do
render_ordered_list(content) |> append_block(iodata) |> build_md(ast)
end
defp build_md(iodata, [{:dl, _, content} | ast]) do
render_description_list(content) |> append_block(iodata) |> build_md(ast)
end
defp append_inline(md, iodata), do: [iodata, md]
defp append_block(md, iodata), do: [iodata, "\n", md, "\n"]
# Renderers
defp render_emphasis(content) do
["*", build_md([], content), "*"]
end
defp render_strong(content) do
["**", build_md([], content), "**"]
end
defp render_code_inline(content) do
["`", build_md([], content), "`"]
end
defp render_link(content, attrs) do
caption = build_md([], content)
if href = attrs[:href] do
["[", caption, "](", href, ")"]
else
caption
end
end
defp render_line_break(), do: "\\\n"
defp render_paragraph(content), do: erlang_html_to_md(content)
defp render_heading(n, content) do
title = build_md([], content)
[String.duplicate("#", n), " ", title]
end
defp render_code_block(content, language) do
["```", language, "\n", content, "\n```"]
end
defp render_blockquote(content) do
inner = erlang_html_to_md(content)
inner
|> String.split("\n")
|> Enum.map_intersperse("\n", &["> ", &1])
end
defp render_unordered_list(content) do
marker_fun = fn _index -> "* " end
render_list(content, marker_fun, " ")
end
defp render_ordered_list(content) do
marker_fun = fn index -> "#{index + 1}. " end
render_list(content, marker_fun, " ")
end
defp render_list(items, marker_fun, indent) do
spaced? = spaced_list_items?(items)
item_separator = if(spaced?, do: "\n\n", else: "\n")
items
|> Enum.map(fn {:li, _, content} -> erlang_html_to_md(content) end)
|> Enum.with_index()
|> Enum.map(fn {inner, index} ->
[first_line | lines] = String.split(inner, "\n")
first_line = marker_fun.(index) <> first_line
lines =
Enum.map(lines, fn
"" -> ""
line -> indent <> line
end)
Enum.intersperse([first_line | lines], "\n")
end)
|> Enum.intersperse(item_separator)
end
defp spaced_list_items?([{:li, _, [{:p, _, _content} | _]} | _items]), do: true
defp spaced_list_items?([_ | items]), do: spaced_list_items?(items)
defp spaced_list_items?([]), do: false
defp render_description_list(content) do
# Rewrite description list as an unordered list with pseudo heading
content
|> Enum.chunk_every(2)
|> Enum.map(fn [{:dt, _, dt}, {:dd, _, dd}] ->
{:li, [], [{:p, [], [{:strong, [], dt}]}, {:p, [], dd}]}
end)
|> render_unordered_list()
end
defp render_types_list(content) do
content
|> group_type_list_items([])
|> render_unordered_list()
end
defp group_type_list_items([], acc), do: Enum.reverse(acc)
defp group_type_list_items([{:li, [{:name, _type_name}], []} | items], acc) do
group_type_list_items(items, acc)
end
defp group_type_list_items([{:li, [{:class, "type"}], content} | items], acc) do
group_type_list_items(items, [{:li, [], [{:code, [], content}]} | acc])
end
defp group_type_list_items(
[{:li, [{:class, "description"}], content} | items],
[{:li, [], prev_content} | acc]
) do
group_type_list_items(items, [{:li, [], prev_content ++ [{:p, [], content}]} | acc])
end
end

View file

@ -1,4 +1,4 @@
defmodule Livebook.Intellisense.IdentifierMatcher do
defmodule Livebook.Intellisense.Elixir.IdentifierMatcher do
# This module allows for extracting information about identifiers
# based on code and runtime information (binding, environment).
#
@ -11,7 +11,7 @@ defmodule Livebook.Intellisense.IdentifierMatcher do
# Server.
alias Livebook.Intellisense
alias Livebook.Intellisense.Docs
alias Livebook.Intellisense.Elixir.Docs
@typedoc """
A single identifier together with relevant information.
@ -525,7 +525,7 @@ defmodule Livebook.Intellisense.IdentifierMatcher do
kind: :module,
module: mod,
display_name: name,
documentation: Intellisense.Docs.get_module_documentation(mod, ctx.node)
documentation: Intellisense.Elixir.Docs.get_module_documentation(mod, ctx.node)
}
end
@ -549,7 +549,7 @@ defmodule Livebook.Intellisense.IdentifierMatcher do
kind: :module,
module: mod,
display_name: name,
documentation: Intellisense.Docs.get_module_documentation(mod, ctx.node)
documentation: Intellisense.Elixir.Docs.get_module_documentation(mod, ctx.node)
}
end
@ -603,7 +603,7 @@ defmodule Livebook.Intellisense.IdentifierMatcher do
kind: :module,
module: mod,
display_name: name,
documentation: Intellisense.Docs.get_module_documentation(mod, ctx.node)
documentation: Intellisense.Elixir.Docs.get_module_documentation(mod, ctx.node)
}
end
@ -682,7 +682,7 @@ defmodule Livebook.Intellisense.IdentifierMatcher do
end)
doc_items =
Intellisense.Docs.lookup_module_members(
Intellisense.Elixir.Docs.lookup_module_members(
mod,
Enum.map(matching_funs, &Tuple.delete_at(&1, 2)),
ctx.node,
@ -755,7 +755,9 @@ defmodule Livebook.Intellisense.IdentifierMatcher do
end)
doc_items =
Intellisense.Docs.lookup_module_members(mod, matching_types, ctx.node, kinds: [:type])
Intellisense.Elixir.Docs.lookup_module_members(mod, matching_types, ctx.node,
kinds: [:type]
)
Enum.map(matching_types, fn {name, arity} ->
doc_item =

View file

@ -1,8 +1,8 @@
defmodule Livebook.Intellisense.SignatureMatcher do
defmodule Livebook.Intellisense.Elixir.SignatureMatcher do
# This module allows for extracting information about function
# signatures matching an incomplete call.
alias Livebook.Intellisense.Docs
alias Livebook.Intellisense.Elixir.Docs
@type signature_info :: {name :: atom(), Docs.signature(), Docs.documentation(), Docs.spec()}
@ -52,7 +52,7 @@ defmodule Livebook.Intellisense.SignatureMatcher do
defp signature_infos_for_members(mod, funs, active_argument, node) do
infos =
Livebook.Intellisense.Docs.lookup_module_members(mod, funs, node,
Livebook.Intellisense.Elixir.Docs.lookup_module_members(mod, funs, node,
kinds: [:function, :macro]
)

View file

@ -0,0 +1,38 @@
defmodule Livebook.Intellisense.Erlang do
alias Livebook.Intellisense
@behaviour Intellisense
@impl true
def handle_request({:format, _code}, _context, _node) do
# Not supported.
nil
end
def handle_request({:completion, hint}, context, node) do
handle_completion(hint, context, node)
end
def handle_request({:details, line, column}, context, node) do
handle_details(line, column, context, node)
end
def handle_request({:signature, hint}, context, node) do
handle_signature(hint, context, node)
end
defp handle_completion(hint, _context, _node) do
# TODO: implement. See t:Livebook.Runtime.completion_response/0 for return type.
nil
end
defp handle_details(_line, _column, _context, _node) do
# TODO: implement. See t:Livebook.Runtime.details_response/0 for return type.
nil
end
defp handle_signature(_hint, _context, _node) do
# TODO: implement. See t:Livebook.Runtime.signature_response/0 for return type.
nil
end
end

View file

@ -41,7 +41,7 @@ defmodule Livebook.Notebook.Cell.Code do
end
@doc """
Return the list of supported langauges for code cells.
Return the list of supported languages for code cells.
"""
@spec languages() :: list(%{name: String.t(), language: atom()})
def languages() do

View file

@ -969,11 +969,12 @@ defprotocol Livebook.Runtime do
@spec handle_intellisense(
t(),
pid(),
language(),
intellisense_request(),
parent_locators(),
{atom(), atom()} | nil
) :: reference()
def handle_intellisense(runtime, send_to, request, parent_locators, node)
def handle_intellisense(runtime, send_to, language, request, parent_locators, node)
@doc """
Reads file at the given absolute path within the runtime file system.

View file

@ -159,8 +159,15 @@ defimpl Livebook.Runtime, for: Livebook.Runtime.Attached do
RuntimeServer.drop_container(runtime.server_pid, container_ref)
end
def handle_intellisense(runtime, send_to, request, parent_locators, node) do
RuntimeServer.handle_intellisense(runtime.server_pid, send_to, request, parent_locators, node)
def handle_intellisense(runtime, send_to, language, request, parent_locators, node) do
RuntimeServer.handle_intellisense(
runtime.server_pid,
send_to,
language,
request,
parent_locators,
node
)
end
def read_file(runtime, path) do

View file

@ -90,8 +90,15 @@ defimpl Livebook.Runtime, for: Livebook.Runtime.Embedded do
RuntimeServer.drop_container(runtime.server_pid, container_ref)
end
def handle_intellisense(runtime, send_to, request, parent_locators, node) do
RuntimeServer.handle_intellisense(runtime.server_pid, send_to, request, parent_locators, node)
def handle_intellisense(runtime, send_to, language, request, parent_locators, node) do
RuntimeServer.handle_intellisense(
runtime.server_pid,
send_to,
language,
request,
parent_locators,
node
)
end
def read_file(runtime, path) do

View file

@ -31,9 +31,11 @@ defmodule Livebook.Runtime.ErlDist do
Livebook.Runtime.Evaluator.Formatter,
Livebook.Runtime.Evaluator.Doctests,
Livebook.Intellisense,
Livebook.Intellisense.Docs,
Livebook.Intellisense.IdentifierMatcher,
Livebook.Intellisense.SignatureMatcher,
Livebook.Intellisense.Elixir,
Livebook.Intellisense.Elixir.Docs,
Livebook.Intellisense.Elixir.IdentifierMatcher,
Livebook.Intellisense.Elixir.SignatureMatcher,
Livebook.Intellisense.Erlang,
Livebook.Runtime.ErlDist,
Livebook.Runtime.ErlDist.NodeManager,
Livebook.Runtime.ErlDist.RuntimeServer,

View file

@ -125,13 +125,19 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServer do
@spec handle_intellisense(
pid(),
pid(),
Runtime.language(),
Runtime.intellisense_request(),
Runtime.Runtime.parent_locators(),
{atom(), atom()} | nil
) :: reference()
def handle_intellisense(pid, send_to, request, parent_locators, node) do
def handle_intellisense(pid, send_to, language, request, parent_locators, node) do
ref = make_ref()
GenServer.cast(pid, {:handle_intellisense, send_to, ref, request, parent_locators, node})
GenServer.cast(
pid,
{:handle_intellisense, send_to, ref, language, request, parent_locators, node}
)
ref
end
@ -546,7 +552,7 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServer do
end
def handle_cast(
{:handle_intellisense, send_to, ref, request, parent_locators, node},
{:handle_intellisense, send_to, ref, language, request, parent_locators, node},
state
) do
{container_ref, parent_evaluation_refs} =
@ -577,7 +583,10 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServer do
Task.Supervisor.start_child(state.task_supervisor, fn ->
node = intellisense_node(node)
response = Livebook.Intellisense.handle_request(request, intellisense_context, node)
response =
Livebook.Intellisense.handle_request(language, request, intellisense_context, node)
send(send_to, {:runtime_intellisense_response, ref, request, response})
end)

View file

@ -426,8 +426,15 @@ defimpl Livebook.Runtime, for: Livebook.Runtime.Fly do
RuntimeServer.drop_container(runtime.server_pid, container_ref)
end
def handle_intellisense(runtime, send_to, request, parent_locators, node) do
RuntimeServer.handle_intellisense(runtime.server_pid, send_to, request, parent_locators, node)
def handle_intellisense(runtime, send_to, language, request, parent_locators, node) do
RuntimeServer.handle_intellisense(
runtime.server_pid,
send_to,
language,
request,
parent_locators,
node
)
end
def read_file(runtime, path) do

View file

@ -365,8 +365,15 @@ defimpl Livebook.Runtime, for: Livebook.Runtime.K8s do
RuntimeServer.drop_container(runtime.server_pid, container_ref)
end
def handle_intellisense(runtime, send_to, request, parent_locators, node) do
RuntimeServer.handle_intellisense(runtime.server_pid, send_to, request, parent_locators, node)
def handle_intellisense(runtime, send_to, language, request, parent_locators, node) do
RuntimeServer.handle_intellisense(
runtime.server_pid,
send_to,
language,
request,
parent_locators,
node
)
end
def read_file(runtime, path) do

View file

@ -283,8 +283,15 @@ defimpl Livebook.Runtime, for: Livebook.Runtime.Standalone do
RuntimeServer.drop_container(runtime.server_pid, container_ref)
end
def handle_intellisense(runtime, send_to, request, parent_locators, node) do
RuntimeServer.handle_intellisense(runtime.server_pid, send_to, request, parent_locators, node)
def handle_intellisense(runtime, send_to, language, request, parent_locators, node) do
RuntimeServer.handle_intellisense(
runtime.server_pid,
send_to,
language,
request,
parent_locators,
node
)
end
def read_file(runtime, path) do

View file

@ -452,7 +452,7 @@ defmodule Livebook.Session do
end
@doc """
Requests the given langauge to be enabled.
Requests the given language to be enabled.
This inserts extra cells and adds dependencies if applicable.
"""
@ -462,7 +462,7 @@ defmodule Livebook.Session do
end
@doc """
Requests the given langauge to be disabled.
Requests the given language to be disabled.
"""
@spec disable_language(pid(), atom()) :: :ok
def disable_language(pid, language) do

View file

@ -131,7 +131,10 @@ defmodule LivebookWeb.Hub.Teams.DeploymentGroupFormComponent do
~H"""
<label class={[
"relative flex rounded-lg border p-4 w-1/2",
if(to_string(@field.value) == to_string(@value), do: "border-blue-500", else: "border-gray-200"),
if(to_string(@field.value) == to_string(@value),
do: "border-blue-500",
else: "border-gray-200"
),
if(@disabled, do: "opacity-70", else: "cursor-pointer")
]}>
<input

View file

@ -659,6 +659,7 @@ defmodule LivebookWeb.SessionLive do
Livebook.Runtime.handle_intellisense(
data.runtime,
self(),
cell.language,
request,
parent_locators,
node

View file

@ -207,11 +207,11 @@ defmodule LivebookWeb.SessionLive.CellComponent do
tag="primary"
empty={@cell_view.empty}
language={@cell_view.language}
intellisense={@cell_view.language == :elixir}
intellisense={@cell_view.language in [:elixir, :erlang]}
/>
</div>
<div class="absolute bottom-2 right-2" data-el-cell-indicators>
<.cell_indicators id={@cell_view.id} cell_view={@cell_view} langauge_toggle />
<.cell_indicators id={@cell_view.id} cell_view={@cell_view} language_toggle />
</div>
</div>
<div :if={@cell_view.language not in @enabled_languages} class="mt-2">
@ -772,7 +772,7 @@ defmodule LivebookWeb.SessionLive.CellComponent do
attr :id, :string, required: true
attr :cell_view, :map, required: true
attr :langauge_toggle, :boolean, default: false
attr :language_toggle, :boolean, default: false
defp cell_indicators(assigns) do
~H"""
@ -780,7 +780,7 @@ defmodule LivebookWeb.SessionLive.CellComponent do
<.cell_indicator :if={has_status?(@cell_view)}>
<.cell_status id={@id} cell_view={@cell_view} />
</.cell_indicator>
<%= if @langauge_toggle do %>
<%= if @language_toggle do %>
<.menu id={"cell-#{@id}-language-menu"} position="bottom-right">
<:toggle>
<.cell_indicator class="cursor-pointer">

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -151,7 +151,7 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServerTest do
describe "handle_intellisense/6 given completion request" do
test "provides basic completion when no evaluation reference is given", %{pid: pid} do
request = {:completion, "System.ver"}
ref = RuntimeServer.handle_intellisense(pid, self(), request, [], nil)
ref = RuntimeServer.handle_intellisense(pid, self(), :elixir, request, [], nil)
assert_receive {:runtime_intellisense_response, ^ref, ^request,
%{items: [%{label: "version/0"}]}}
@ -169,7 +169,7 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServerTest do
request = {:completion, "num"}
ref =
RuntimeServer.handle_intellisense(pid, self(), request, [{:c1, :e1}], nil)
RuntimeServer.handle_intellisense(pid, self(), :elixir, request, [{:c1, :e1}], nil)
assert_receive {:runtime_intellisense_response, ^ref, ^request,
%{items: [%{label: "number"}]}}
@ -177,7 +177,7 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServerTest do
request = {:completion, "ANSI.brigh"}
ref =
RuntimeServer.handle_intellisense(pid, self(), request, [{:c1, :e1}], nil)
RuntimeServer.handle_intellisense(pid, self(), :elixir, request, [{:c1, :e1}], nil)
assert_receive {:runtime_intellisense_response, ^ref, ^request,
%{items: [%{label: "bright/0"}]}}
@ -187,7 +187,7 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServerTest do
describe "handle_intellisense/6 given details request" do
test "responds with identifier details", %{pid: pid} do
request = {:details, "System.version", 10}
ref = RuntimeServer.handle_intellisense(pid, self(), request, [], nil)
ref = RuntimeServer.handle_intellisense(pid, self(), :elixir, request, [], nil)
assert_receive {:runtime_intellisense_response, ^ref, ^request,
%{range: %{from: 1, to: 15}, contents: [_]}}
@ -197,7 +197,7 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServerTest do
describe "handle_intellisense/6 given format request" do
test "responds with a formatted code", %{pid: pid} do
request = {:format, "System.version"}
ref = RuntimeServer.handle_intellisense(pid, self(), request, [], nil)
ref = RuntimeServer.handle_intellisense(pid, self(), :elixir, request, [], nil)
assert_receive {:runtime_intellisense_response, ^ref, ^request, %{code: "System.version()"}}
end

View file

@ -368,8 +368,7 @@ defmodule Livebook.Session.DataTest do
"s2" => %{evaluating_cell_id: nil},
"s3" => %{evaluating_cell_id: nil}
}
} = new_data,
[{:stop_evaluation, %{id: "s2", parent_id: nil}}]} =
} = new_data, [{:stop_evaluation, %{id: "s2", parent_id: nil}}]} =
Data.apply_operation(data, operation)
assert new_data.section_infos["s1"].evaluation_queue == MapSet.new([])
@ -655,8 +654,7 @@ defmodule Livebook.Session.DataTest do
deleted_at: _
}
]
},
[{:forget_evaluation, %{id: "c2"}, %{id: "s2"}}]} =
}, [{:forget_evaluation, %{id: "c2"}, %{id: "s2"}}]} =
Data.apply_operation(data, operation)
end
@ -1564,8 +1562,7 @@ defmodule Livebook.Session.DataTest do
assert {:ok,
%{
notebook: %{setup_section: %{cells: [%Notebook.Cell.Code{}]}}
},
[{:forget_evaluation, %{id: @pyproject_setup_id}, %{id: "setup-section"}}]} =
}, [{:forget_evaluation, %{id: @pyproject_setup_id}, %{id: "setup-section"}}]} =
Data.apply_operation(data, operation)
end
@ -1913,8 +1910,7 @@ defmodule Livebook.Session.DataTest do
section_infos: %{
"s2" => %{evaluating_cell_id: "c2"}
}
} = new_data,
[{:start_evaluation, %{id: "c2"}, %{id: "s2"}, []}]} =
} = new_data, [{:start_evaluation, %{id: "c2"}, %{id: "s2"}, []}]} =
Data.apply_operation(data, operation)
assert new_data.section_infos["s2"].evaluation_queue == MapSet.new([])
@ -1999,8 +1995,7 @@ defmodule Livebook.Session.DataTest do
cell_infos: %{
"c1" => %{eval: %{status: :evaluating, evaluation_opts: ^evaluation_opts}}
}
} = new_data,
[{:start_evaluation, %{id: "c1"}, %{id: "s1"}, ^evaluation_opts}]} =
} = new_data, [{:start_evaluation, %{id: "c1"}, %{id: "s1"}, ^evaluation_opts}]} =
Data.apply_operation(data, operation)
assert new_data.section_infos["s1"].evaluation_queue == MapSet.new([])
@ -2026,8 +2021,7 @@ defmodule Livebook.Session.DataTest do
cell_infos: %{
@setup_id => %{eval: %{status: :queued, evaluation_opts: ^evaluation_opts}}
}
} = new_data,
[{:disconnect_runtime, ^runtime}, :connect_runtime]} =
} = new_data, [{:disconnect_runtime, ^runtime}, :connect_runtime]} =
Data.apply_operation(data, operation)
assert new_data.section_infos["s1"].evaluation_queue == MapSet.new([])
@ -2231,8 +2225,7 @@ defmodule Livebook.Session.DataTest do
section_infos: %{
"s1" => %{evaluating_cell_id: "c2"}
}
} = new_data,
[{:start_evaluation, %{id: "c2"}, %{id: "s1"}, []}]} =
} = new_data, [{:start_evaluation, %{id: "c2"}, %{id: "s1"}, []}]} =
Data.apply_operation(data, operation)
assert new_data.section_infos["s1"].evaluation_queue == MapSet.new([])
@ -2259,8 +2252,7 @@ defmodule Livebook.Session.DataTest do
"s1" => %{evaluating_cell_id: nil},
"s2" => %{evaluating_cell_id: "c2"}
}
} = new_data,
[{:start_evaluation, %{id: "c2"}, %{id: "s2"}, []}]} =
} = new_data, [{:start_evaluation, %{id: "c2"}, %{id: "s2"}, []}]} =
Data.apply_operation(data, operation)
assert new_data.section_infos["s1"].evaluation_queue == MapSet.new([])
@ -3165,8 +3157,7 @@ defmodule Livebook.Session.DataTest do
assert {:ok,
%{
notebook: %{sections: [%{cells: [%{id: "c1", source: "content"}]}]}
},
[{:report_delta, ^client_id, _cell, :primary, ^delta, nil}]} =
}, [{:report_delta, ^client_id, _cell, :primary, ^delta, nil}]} =
Data.apply_operation(data, operation)
end
end
@ -3195,8 +3186,7 @@ defmodule Livebook.Session.DataTest do
notebook: %{
sections: [%{cells: [%{id: "c1", source: "content!", attrs: ^attrs}]}]
}
},
[{:report_delta, ^client_id, _cell, :primary, ^delta2, nil}]} =
}, [{:report_delta, ^client_id, _cell, :primary, ^delta2, nil}]} =
Data.apply_operation(data, operation)
end
end
@ -4075,8 +4065,7 @@ defmodule Livebook.Session.DataTest do
section_infos: %{
"setup-section" => %{evaluating_cell_id: @setup_id}
}
} = new_data,
[{:start_evaluation, %{id: @setup_id}, %{id: "setup-section"}, []}]} =
} = new_data, [{:start_evaluation, %{id: @setup_id}, %{id: "setup-section"}, []}]} =
Data.apply_operation(data, operation)
assert new_data.section_infos["setup-section"].evaluation_queue == MapSet.new([])
@ -4630,8 +4619,7 @@ defmodule Livebook.Session.DataTest do
"c2" => %{eval: %{status: :queued}}
},
app_data: %{status: %{execution: :executing}}
},
[:app_report_status, {:disconnect_runtime, _}, :connect_runtime]} =
}, [:app_report_status, {:disconnect_runtime, _}, :connect_runtime]} =
Data.apply_operation(data, operation)
end

View file

@ -674,7 +674,7 @@ defmodule LivebookWeb.SessionLiveTest do
Session.get_data(session.pid)
end
test "shows an error when a cell langauge is not enabled", %{conn: conn, session: session} do
test "shows an error when a cell language is not enabled", %{conn: conn, session: session} do
section_id = insert_section(session.pid)
cell_id = insert_text_cell(session.pid, section_id, :code)

View file

@ -28,7 +28,7 @@ defmodule Livebook.Runtime.NoopRuntime do
def evaluate_code(_, _, _, _, _, _ \\ []), do: :ok
def forget_evaluation(_, _), do: :ok
def drop_container(_, _), do: :ok
def handle_intellisense(_, _, _, _, _), do: make_ref()
def handle_intellisense(_, _, _, _, _, _), do: make_ref()
def read_file(_, path) do
case File.read(path) do

View file

@ -148,4 +148,21 @@ defmodule Livebook.TestHelpers do
defp remove_ansi(string) do
String.replace(string, ~r/\e\[\d+m/, "")
end
# Returns intellisense context resulting from evaluating
# the given block of code in a fresh context.
defmacro intellisense_context_from_eval(ebin_path \\ System.tmp_dir!(), do: block) do
quote do
block = unquote(Macro.escape(block))
binding = []
env = Code.env_for_eval([])
{value, binding, env} = Code.eval_quoted_with_env(block, binding, env)
%{
env: env,
ebin_path: unquote(ebin_path),
map_binding: fn fun -> fun.(binding) end
}
end
end
end