Add link to Hexdocs in hover docs (#1221)

Co-authored-by: Jonatan Kłosko <jonatanklosko@gmail.com>
This commit is contained in:
ByeongUk Choi 2022-06-14 08:21:42 +09:00 committed by GitHub
parent 49e7317ea4
commit 8f4cafe264
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 208 additions and 91 deletions

View file

@ -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,26 +148,36 @@ defmodule Livebook.Intellisense do
insert_text: Atom.to_string(name)
}
defp format_completion_item({:in_struct_field, struct, name, default}),
do: %{
label: Atom.to_string(name),
kind: :field,
detail: "#{inspect(struct)} struct field",
documentation:
join_with_divider([
code(name),
"""
**Default**
defp format_completion_item(%{
kind: :in_struct_field,
struct: struct,
name: name,
default: default
}),
do: %{
label: Atom.to_string(name),
kind: :field,
detail: "#{inspect(struct)} struct field",
documentation:
join_with_divider([
code(name),
"""
**Default**
```
#{inspect(default, pretty: true, width: @line_length)}
```
"""
]),
insert_text: "#{name}: "
}
```
#{inspect(default, pretty: true, width: @line_length)}
```
"""
]),
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,23 +243,28 @@ defmodule Livebook.Intellisense do
end
}
defp format_completion_item({:type, _module, name, arity, documentation}),
do: %{
label: "#{name}/#{arity}",
kind: :type,
detail: "typespec",
documentation: format_documentation(documentation, :short),
insert_text: Atom.to_string(name)
}
defp format_completion_item(%{
kind: :type,
name: name,
arity: arity,
documentation: documentation
}),
do: %{
label: "#{name}/#{arity}",
kind: :type,
detail: "typespec",
documentation: format_documentation(documentation, :short),
insert_text: Atom.to_string(name)
}
defp format_completion_item({:module_attribute, name, documentation}),
do: %{
label: Atom.to_string(name),
kind: :variable,
detail: "module attribute",
documentation: format_documentation(documentation, :short),
insert_text: Atom.to_string(name)
}
defp format_completion_item(%{kind: :module_attribute, name: name, documentation: documentation}),
do: %{
label: Atom.to_string(name),
kind: :variable,
detail: "module attribute",
documentation: format_documentation(documentation, :short),
insert_text: Atom.to_string(name)
}
defp keyword_macro?(name) do
def? = name |> Atom.to_string() |> String.starts_with?("def")
@ -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

View file

@ -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}, []),

View file

@ -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
# ---

View file

@ -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.

View file

@ -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