mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-09-06 21:14:26 +08:00
Add link to Hexdocs in hover docs (#1221)
Co-authored-by: Jonatan Kłosko <jonatanklosko@gmail.com>
This commit is contained in:
parent
49e7317ea4
commit
8f4cafe264
5 changed files with 208 additions and 91 deletions
|
@ -126,17 +126,11 @@ defmodule Livebook.Intellisense do
|
|||
|> Enum.sort_by(&completion_item_priority/1)
|
||||
end
|
||||
|
||||
defp include_in_completion?({:module, _module, _display_name, :hidden}), do: false
|
||||
|
||||
defp include_in_completion?(
|
||||
{:function, _module, _name, _arity, _type, _display_name, :hidden, _signatures, _specs,
|
||||
_meta}
|
||||
),
|
||||
do: false
|
||||
|
||||
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({:variable, name}),
|
||||
defp format_completion_item(%{kind: :variable, name: name}),
|
||||
do: %{
|
||||
label: Atom.to_string(name),
|
||||
kind: :variable,
|
||||
|
@ -145,7 +139,7 @@ defmodule Livebook.Intellisense do
|
|||
insert_text: Atom.to_string(name)
|
||||
}
|
||||
|
||||
defp format_completion_item({:map_field, name}),
|
||||
defp format_completion_item(%{kind: :map_field, name: name}),
|
||||
do: %{
|
||||
label: Atom.to_string(name),
|
||||
kind: :field,
|
||||
|
@ -154,7 +148,12 @@ defmodule Livebook.Intellisense do
|
|||
insert_text: Atom.to_string(name)
|
||||
}
|
||||
|
||||
defp format_completion_item({:in_struct_field, struct, name, default}),
|
||||
defp format_completion_item(%{
|
||||
kind: :in_struct_field,
|
||||
struct: struct,
|
||||
name: name,
|
||||
default: default
|
||||
}),
|
||||
do: %{
|
||||
label: Atom.to_string(name),
|
||||
kind: :field,
|
||||
|
@ -173,7 +172,12 @@ defmodule Livebook.Intellisense do
|
|||
insert_text: "#{name}: "
|
||||
}
|
||||
|
||||
defp format_completion_item({:module, module, display_name, documentation}) do
|
||||
defp format_completion_item(%{
|
||||
kind: :module,
|
||||
module: module,
|
||||
display_name: display_name,
|
||||
documentation: documentation
|
||||
}) do
|
||||
subtype = Docs.get_module_subtype(module)
|
||||
|
||||
kind =
|
||||
|
@ -196,10 +200,17 @@ defmodule Livebook.Intellisense do
|
|||
}
|
||||
end
|
||||
|
||||
defp format_completion_item(
|
||||
{:function, module, name, arity, type, display_name, documentation, signatures, specs,
|
||||
_meta}
|
||||
),
|
||||
defp format_completion_item(%{
|
||||
kind: :function,
|
||||
module: module,
|
||||
name: name,
|
||||
arity: arity,
|
||||
type: type,
|
||||
display_name: display_name,
|
||||
documentation: documentation,
|
||||
signatures: signatures,
|
||||
specs: specs
|
||||
}),
|
||||
do: %{
|
||||
label: "#{display_name}/#{arity}",
|
||||
kind: :function,
|
||||
|
@ -232,7 +243,12 @@ defmodule Livebook.Intellisense do
|
|||
end
|
||||
}
|
||||
|
||||
defp format_completion_item({:type, _module, name, arity, documentation}),
|
||||
defp format_completion_item(%{
|
||||
kind: :type,
|
||||
name: name,
|
||||
arity: arity,
|
||||
documentation: documentation
|
||||
}),
|
||||
do: %{
|
||||
label: "#{name}/#{arity}",
|
||||
kind: :type,
|
||||
|
@ -241,7 +257,7 @@ defmodule Livebook.Intellisense do
|
|||
insert_text: Atom.to_string(name)
|
||||
}
|
||||
|
||||
defp format_completion_item({:module_attribute, name, documentation}),
|
||||
defp format_completion_item(%{kind: :module_attribute, name: name, documentation: documentation}),
|
||||
do: %{
|
||||
label: Atom.to_string(name),
|
||||
kind: :variable,
|
||||
|
@ -357,18 +373,21 @@ defmodule Livebook.Intellisense do
|
|||
%{matches: matches, range: range} ->
|
||||
contents =
|
||||
matches
|
||||
|> Enum.filter(&include_in_details?/1)
|
||||
|> Enum.map(&format_details_item/1)
|
||||
|> Enum.uniq()
|
||||
|
||||
%{range: range, contents: contents}
|
||||
end
|
||||
end
|
||||
|
||||
defp format_details_item({:variable, name}), do: code(name)
|
||||
defp include_in_details?(%{kind: :function, from_default: true}), do: false
|
||||
defp include_in_details?(_), do: true
|
||||
|
||||
defp format_details_item({:map_field, name}), do: code(name)
|
||||
defp format_details_item(%{kind: :variable, name: name}), do: code(name)
|
||||
|
||||
defp format_details_item({:in_struct_field, _struct, name, default}) do
|
||||
defp format_details_item(%{kind: :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),
|
||||
"""
|
||||
|
@ -381,34 +400,44 @@ defmodule Livebook.Intellisense do
|
|||
])
|
||||
end
|
||||
|
||||
defp format_details_item({:module, module, _display_name, documentation}) do
|
||||
defp format_details_item(%{kind: :module, module: module, documentation: documentation}) do
|
||||
join_with_divider([
|
||||
code(inspect(module)),
|
||||
format_hexdocs_link(module),
|
||||
format_documentation(documentation, :all)
|
||||
])
|
||||
end
|
||||
|
||||
defp format_details_item(
|
||||
{:function, module, name, _arity, _type, _display_name, documentation, signatures, specs,
|
||||
meta}
|
||||
) do
|
||||
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) |> code(),
|
||||
format_meta(:since, meta),
|
||||
join_with_middle_dot([
|
||||
format_hexdocs_link(module, "#{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({:type, _module, name, _arity, documentation}) do
|
||||
defp format_details_item(%{kind: :type, name: name, documentation: documentation}) do
|
||||
join_with_divider([
|
||||
code(name),
|
||||
format_documentation(documentation, :all)
|
||||
])
|
||||
end
|
||||
|
||||
defp format_details_item({:module_attribute, name, documentation}) do
|
||||
defp format_details_item(%{kind: :module_attribute, name: name, documentation: documentation}) do
|
||||
join_with_divider([
|
||||
code("@#{name}"),
|
||||
format_documentation(documentation, :all)
|
||||
|
@ -421,6 +450,8 @@ defmodule Livebook.Intellisense do
|
|||
|
||||
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
|
||||
|
@ -438,6 +469,17 @@ defmodule Livebook.Intellisense do
|
|||
"""
|
||||
end
|
||||
|
||||
defp format_hexdocs_link(module, hash \\ "") do
|
||||
hash = if hash == "", do: "", else: "#" <> hash
|
||||
|
||||
app = Application.get_application(module)
|
||||
|
||||
if vsn = app && Application.spec(app, :vsn) do
|
||||
url = "https://hexdocs.pm/#{app}/#{vsn}/#{inspect(module)}.html#{hash}"
|
||||
"[Hexdocs](#{url})"
|
||||
end
|
||||
end
|
||||
|
||||
defp format_signatures([], _module), do: nil
|
||||
|
||||
defp format_signatures(signatures, module) do
|
||||
|
|
|
@ -8,6 +8,7 @@ defmodule Livebook.Intellisense.Docs do
|
|||
kind: member_kind(),
|
||||
name: atom(),
|
||||
arity: non_neg_integer(),
|
||||
from_default: boolean(),
|
||||
documentation: documentation(),
|
||||
signatures: list(signature()),
|
||||
specs: list(spec()),
|
||||
|
@ -89,6 +90,7 @@ defmodule Livebook.Intellisense.Docs do
|
|||
kind: kind,
|
||||
name: name,
|
||||
arity: arity,
|
||||
from_default: arity != base_arity,
|
||||
documentation: documentation(doc, format),
|
||||
signatures: signatures,
|
||||
specs: Map.get(specs, {name, base_arity}, []),
|
||||
|
|
|
@ -20,19 +20,54 @@ defmodule Livebook.Intellisense.IdentifierMatcher do
|
|||
A single identifier together with relevant information.
|
||||
"""
|
||||
@type identifier_item ::
|
||||
{:variable, name()}
|
||||
| {:map_field, name()}
|
||||
| {:in_struct_field, module(), name(), default :: value()}
|
||||
| {:module, module(), display_name(), Docs.documentation()}
|
||||
| {:function, module(), name(), arity(), function_type(), display_name(),
|
||||
Docs.documentation(), list(Docs.signature()), list(Docs.spec()), Docs.meta()}
|
||||
| {:type, module(), name(), arity(), Docs.documentation()}
|
||||
| {:module_attribute, name(), Docs.documentation()}
|
||||
%{
|
||||
kind: :variable,
|
||||
name: name()
|
||||
}
|
||||
| %{
|
||||
kind: :map_field,
|
||||
name: name()
|
||||
}
|
||||
| %{
|
||||
kind: :in_struct_field,
|
||||
module: module(),
|
||||
name: name(),
|
||||
default: term()
|
||||
}
|
||||
| %{
|
||||
kind: :module,
|
||||
module: module(),
|
||||
display_name: display_name(),
|
||||
documentation: Docs.documentation()
|
||||
}
|
||||
| %{
|
||||
kind: :function,
|
||||
module: module(),
|
||||
name: name(),
|
||||
arity: arity(),
|
||||
type: :function | :macro,
|
||||
display_name: display_name(),
|
||||
from_default: boolean(),
|
||||
documentation: Docs.documentation(),
|
||||
signatures: list(Docs.signature()),
|
||||
specs: list(Docs.spec()),
|
||||
meta: Docs.meta()
|
||||
}
|
||||
| %{
|
||||
kind: :type,
|
||||
module: module(),
|
||||
name: name(),
|
||||
arity: arity(),
|
||||
documentation: Docs.documentation()
|
||||
}
|
||||
| %{
|
||||
kind: :module_attribute,
|
||||
name: name(),
|
||||
documentation: Docs.documentation()
|
||||
}
|
||||
|
||||
@type name :: atom()
|
||||
@type display_name :: String.t()
|
||||
@type value :: term()
|
||||
@type function_type :: :function | :macro
|
||||
|
||||
@exact_matcher &Kernel.==/2
|
||||
@prefix_matcher &String.starts_with?/2
|
||||
|
@ -231,10 +266,10 @@ defmodule Livebook.Intellisense.IdentifierMatcher do
|
|||
end
|
||||
|
||||
defp match_struct(hint, ctx) do
|
||||
for {:module, module, name, documentation} <- match_alias(hint, ctx, true),
|
||||
for %{kind: :module, module: module} = item <- match_alias(hint, ctx, true),
|
||||
has_struct?(module),
|
||||
not is_exception?(module),
|
||||
do: {:module, module, name, documentation}
|
||||
do: item
|
||||
end
|
||||
|
||||
defp has_struct?(mod) do
|
||||
|
@ -255,7 +290,7 @@ defmodule Livebook.Intellisense.IdentifierMatcher do
|
|||
for {field, default} <- fields,
|
||||
name = Atom.to_string(field),
|
||||
ctx.matcher.(name, hint),
|
||||
do: {:in_struct_field, struct, field, default}
|
||||
do: %{kind: :in_struct_field, struct: struct, name: field, default: default}
|
||||
|
||||
_ ->
|
||||
match_local_or_var(hint, ctx)
|
||||
|
@ -321,7 +356,7 @@ defmodule Livebook.Intellisense.IdentifierMatcher do
|
|||
for {var, nil} <- Macro.Env.vars(ctx.intellisense_context.env),
|
||||
name = Atom.to_string(var),
|
||||
ctx.matcher.(name, hint),
|
||||
do: {:variable, var}
|
||||
do: %{kind: :variable, name: var}
|
||||
end
|
||||
|
||||
defp match_map_field(map, hint, ctx) do
|
||||
|
@ -330,25 +365,26 @@ defmodule Livebook.Intellisense.IdentifierMatcher do
|
|||
is_atom(key),
|
||||
name = Atom.to_string(key),
|
||||
ctx.matcher.(name, hint),
|
||||
do: {:map_field, key}
|
||||
do: %{kind: :map_field, name: key}
|
||||
end
|
||||
|
||||
defp match_sigil(hint, ctx) do
|
||||
for {:function, module, name, arity, type, "sigil_" <> sigil_name, documentation, signatures,
|
||||
specs,
|
||||
meta} <-
|
||||
for %{kind: :function, display_name: "sigil_" <> sigil_name} = item <-
|
||||
match_local("sigil_", %{ctx | matcher: @prefix_matcher}),
|
||||
ctx.matcher.(sigil_name, hint),
|
||||
do:
|
||||
{:function, module, name, arity, type, "~" <> sigil_name, documentation, signatures,
|
||||
specs, meta}
|
||||
do: %{item | display_name: "~" <> sigil_name}
|
||||
end
|
||||
|
||||
defp match_erlang_module(hint, ctx) do
|
||||
for mod <- get_matching_modules(hint, ctx),
|
||||
usable_as_unquoted_module?(mod),
|
||||
name = ":" <> Atom.to_string(mod),
|
||||
do: {:module, mod, name, Intellisense.Docs.get_module_documentation(mod)}
|
||||
do: %{
|
||||
kind: :module,
|
||||
module: mod,
|
||||
display_name: name,
|
||||
documentation: Intellisense.Docs.get_module_documentation(mod)
|
||||
}
|
||||
end
|
||||
|
||||
# Converts alias string to module atom with regard to the given env
|
||||
|
@ -366,7 +402,12 @@ defmodule Livebook.Intellisense.IdentifierMatcher do
|
|||
for {alias, mod} <- ctx.intellisense_context.env.aliases,
|
||||
[name] = Module.split(alias),
|
||||
ctx.matcher.(name, hint),
|
||||
do: {:module, mod, name, Intellisense.Docs.get_module_documentation(mod)}
|
||||
do: %{
|
||||
kind: :module,
|
||||
module: mod,
|
||||
display_name: name,
|
||||
documentation: Intellisense.Docs.get_module_documentation(mod)
|
||||
}
|
||||
end
|
||||
|
||||
defp match_module(base_mod, hint, nested?, ctx) do
|
||||
|
@ -390,7 +431,7 @@ defmodule Livebook.Intellisense.IdentifierMatcher do
|
|||
# `Elixir` is not a existing module name, but `Elixir.Enum` is,
|
||||
# so if the user types `Eli` the completion should include `Elixir`.
|
||||
if ctx.matcher.("Elixir", hint) do
|
||||
[{:module, Elixir, "Elixir", nil} | items]
|
||||
[%{kind: :module, module: Elixir, display_name: "Elixir", documentation: nil} | items]
|
||||
else
|
||||
items
|
||||
end
|
||||
|
@ -415,7 +456,12 @@ defmodule Livebook.Intellisense.IdentifierMatcher do
|
|||
valid_alias_piece?("." <> name),
|
||||
mod = Module.concat(parent_mod_parts ++ name_parts),
|
||||
uniq: true,
|
||||
do: {:module, mod, name, Intellisense.Docs.get_module_documentation(mod)}
|
||||
do: %{
|
||||
kind: :module,
|
||||
module: mod,
|
||||
display_name: name,
|
||||
documentation: Intellisense.Docs.get_module_documentation(mod)
|
||||
}
|
||||
end
|
||||
|
||||
defp valid_alias_piece?(<<?., char, rest::binary>>) when char in ?A..?Z,
|
||||
|
@ -489,14 +535,25 @@ defmodule Livebook.Intellisense.IdentifierMatcher do
|
|||
doc_item =
|
||||
Enum.find(
|
||||
doc_items,
|
||||
%{documentation: nil, signatures: [], specs: [], meta: %{}},
|
||||
%{from_default: false, documentation: nil, signatures: [], specs: [], meta: %{}},
|
||||
fn doc_item ->
|
||||
doc_item.name == name && doc_item.arity == arity
|
||||
end
|
||||
)
|
||||
|
||||
{:function, mod, name, arity, type, Atom.to_string(name), doc_item.documentation,
|
||||
doc_item.signatures, doc_item.specs, doc_item.meta}
|
||||
%{
|
||||
kind: :function,
|
||||
module: mod,
|
||||
name: name,
|
||||
arity: arity,
|
||||
type: type,
|
||||
display_name: Atom.to_string(name),
|
||||
from_default: doc_item.from_default,
|
||||
documentation: doc_item.documentation,
|
||||
signatures: doc_item.signatures,
|
||||
specs: doc_item.specs,
|
||||
meta: doc_item.meta
|
||||
}
|
||||
end)
|
||||
else
|
||||
[]
|
||||
|
@ -535,7 +592,7 @@ defmodule Livebook.Intellisense.IdentifierMatcher do
|
|||
doc_item.name == name && doc_item.arity == arity
|
||||
end)
|
||||
|
||||
{:type, mod, name, arity, doc_item.documentation}
|
||||
%{kind: :type, module: mod, name: name, arity: arity, documentation: doc_item.documentation}
|
||||
end)
|
||||
end
|
||||
|
||||
|
@ -578,7 +635,11 @@ defmodule Livebook.Intellisense.IdentifierMatcher do
|
|||
for {attribute, info} <- Module.reserved_attributes(),
|
||||
name = Atom.to_string(attribute),
|
||||
ctx.matcher.(name, hint),
|
||||
do: {:module_attribute, attribute, {"text/markdown", info.doc}}
|
||||
do: %{
|
||||
kind: :module_attribute,
|
||||
name: attribute,
|
||||
documentation: {"text/markdown", info.doc}
|
||||
}
|
||||
end
|
||||
|
||||
# ---
|
||||
|
|
|
@ -10,7 +10,7 @@ defmodule Livebook.Intellisense.SignatureMatcher do
|
|||
|
||||
@doc """
|
||||
Looks up a list of signatures matching the given incomplete
|
||||
funciton call.
|
||||
function call.
|
||||
|
||||
Evaluation binding and environment is used to expand aliases,
|
||||
imports, access variable values, etc.
|
||||
|
|
|
@ -1251,6 +1251,18 @@ defmodule Livebook.IntellisenseTest do
|
|||
assert %{contents: [date_range]} = Intellisense.get_details("Date.Range", 8, context)
|
||||
assert date_range =~ "Date.Range"
|
||||
end
|
||||
|
||||
test "returns link to hexdocs" do
|
||||
context = eval(do: nil)
|
||||
|
||||
assert %{contents: [content]} = Intellisense.get_details("Integer", 1, context)
|
||||
assert content =~ ~r"https://hexdocs.pm/elixir/[^/]+/Integer.html"
|
||||
|
||||
assert %{contents: [content]} =
|
||||
Intellisense.get_details("Integer.to_string(10)", 15, context)
|
||||
|
||||
assert content =~ ~r"https://hexdocs.pm/elixir/[^/]+/Integer.html#to_string/2"
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_signature_items/3" do
|
||||
|
|
Loading…
Add table
Reference in a new issue