mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-11-22 17:40:37 +08:00
Add per-language dispatch to intellisense (#3096)
This commit is contained in:
parent
64b03396e3
commit
07cb0ebe89
26 changed files with 3577 additions and 3023 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
apply(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
|
||||
|
|
|
|||
637
lib/livebook/intellisense/elixir.ex
Normal file
637
lib/livebook/intellisense/elixir.ex
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
@ -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 =
|
||||
|
|
@ -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]
|
||||
)
|
||||
|
||||
38
lib/livebook/intellisense/erlang.ex
Normal file
38
lib/livebook/intellisense/erlang.ex
Normal 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)
|
||||
end
|
||||
|
||||
def handle_request({:details, line, column}, context, _node) do
|
||||
handle_details(line, column, context)
|
||||
end
|
||||
|
||||
def handle_request({:signature, hint}, context, _node) do
|
||||
handle_signature(hint, context)
|
||||
end
|
||||
|
||||
defp handle_completion(_hint, _context) do
|
||||
# TODO: implement. See t:Livebook.Runtime.completion_response/0 for return type.
|
||||
nil
|
||||
end
|
||||
|
||||
defp handle_details(_line, _column, _context) do
|
||||
# TODO: implement. See t:Livebook.Runtime.details_response/0 for return type.
|
||||
nil
|
||||
end
|
||||
|
||||
defp handle_signature(_hint, _context) do
|
||||
# TODO: implement. See t:Livebook.Runtime.signature_response/0 for return type.
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -659,6 +659,7 @@ defmodule LivebookWeb.SessionLive do
|
|||
Livebook.Runtime.handle_intellisense(
|
||||
data.runtime,
|
||||
self(),
|
||||
cell.language,
|
||||
request,
|
||||
parent_locators,
|
||||
node
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
2504
test/livebook/intellisense/elixir_test.exs
Normal file
2504
test/livebook/intellisense/elixir_test.exs
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue