Add on-hover details for types (#1974)

This commit is contained in:
awerment 2023-06-12 14:17:25 +02:00 committed by GitHub
parent b2e21aeac2
commit 7691324a4e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 118 additions and 9 deletions

View file

@ -458,7 +458,7 @@ defmodule Livebook.Intellisense do
join_with_divider([
format_signatures(signatures, module) |> code(),
join_with_middle_dot([
format_docs_link(module, name, arity),
format_docs_link(module, {:function, name, arity}),
format_meta(:since, meta)
]),
format_meta(:deprecated, meta),
@ -467,9 +467,18 @@ defmodule Livebook.Intellisense do
])
end
defp format_details_item(%{kind: :type, name: name, documentation: documentation}) do
defp format_details_item(%{
kind: :type,
module: module,
name: name,
arity: arity,
documentation: documentation,
type_spec: type_spec
}) do
join_with_divider([
code(name),
format_type_signature(type_spec, module) |> code(),
format_docs_link(module, {:type, name, arity}),
format_type_spec(type_spec, @extended_line_length) |> code(),
format_documentation(documentation, :all)
])
end
@ -506,7 +515,7 @@ defmodule Livebook.Intellisense do
"""
end
defp format_docs_link(module, function \\ nil, arity \\ nil) do
defp format_docs_link(module, function_or_type \\ nil) do
app = Application.get_application(module)
module_name =
@ -524,12 +533,24 @@ defmodule Livebook.Intellisense do
cond do
is_otp? ->
hash = if function, do: "##{function}-#{arity}", else: ""
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 = if function, do: "##{function}/#{arity}", else: ""
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})"
@ -551,6 +572,13 @@ defmodule Livebook.Intellisense do
end
end
defp format_type_signature(nil, _module), do: nil
defp format_type_signature({_type_kind, type}, module) do
{:"::", _env, [lhs, _rhs]} = Code.Typespec.type_to_quoted(type)
inspect(module) <> "." <> Macro.to_string(lhs)
end
defp format_meta(:deprecated, %{deprecated: deprecated}) do
"**Deprecated**. " <> deprecated
end
@ -582,6 +610,27 @@ defmodule Livebook.Intellisense do
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

View file

@ -12,6 +12,7 @@ defmodule Livebook.Intellisense.Docs do
documentation: documentation(),
signatures: list(signature()),
specs: list(spec()),
type_spec: type_spec(),
meta: meta()
}
@ -28,6 +29,13 @@ defmodule Livebook.Intellisense.Docs do
"""
@type spec :: term()
@typedoc """
A tuple containing a single type annotation in the Erlang Abstract Format,
tagged by its kind.
"""
@type type_spec() :: {type_kind(), term()}
@type type_kind() :: :type | :opaque
@doc """
Fetches documentation for the given module if available.
"""
@ -80,6 +88,17 @@ defmodule Livebook.Intellisense.Docs do
_ -> %{}
end
type_specs =
with true <- :type in kinds,
{:ok, types} <- Code.Typespec.fetch_types(module) do
for {type_kind, {name, _defs, vars}} = type <- types,
type_kind in [:type, :opaque],
into: Map.new(),
do: {{name, Enum.count(vars)}, type}
else
_ -> %{}
end
case Code.fetch_docs(module) do
{:docs_v1, _, _, format, _, _, docs} ->
for {{kind, name, base_arity}, _line, signatures, doc, meta} <- docs,
@ -95,6 +114,7 @@ defmodule Livebook.Intellisense.Docs do
documentation: documentation(doc, format),
signatures: signatures,
specs: Map.get(specs, {name, base_arity}, []),
type_spec: Map.get(type_specs, {name, base_arity}, nil),
meta: meta
}

View file

@ -62,7 +62,8 @@ defmodule Livebook.Intellisense.IdentifierMatcher do
module: module(),
name: name(),
arity: arity(),
documentation: Docs.documentation()
documentation: Docs.documentation(),
type_spec: Docs.type_spec()
}
| %{
kind: :module_attribute,
@ -710,11 +711,18 @@ defmodule Livebook.Intellisense.IdentifierMatcher do
Enum.map(matching_types, fn {name, arity} ->
doc_item =
Enum.find(doc_items, %{documentation: nil}, fn doc_item ->
Enum.find(doc_items, %{documentation: nil, type_spec: nil}, fn doc_item ->
doc_item.name == name && doc_item.arity == arity
end)
%{kind: :type, module: mod, name: name, arity: arity, documentation: doc_item.documentation}
%{
kind: :type,
module: mod,
name: name,
arity: arity,
documentation: doc_item.documentation,
type_spec: doc_item.type_spec
}
end)
end

View file

@ -1391,6 +1391,30 @@ defmodule Livebook.IntellisenseTest do
assert date_range =~ "Date.Range"
end
test "returns module-prepended type signatures" do
context = eval(do: nil)
assert %{contents: [type]} = Intellisense.get_details("Date.t", 6, context)
assert type =~ "Date.t()"
assert %{contents: [type]} = Intellisense.get_details(":code.load_error_rsn", 8, context)
assert type =~ ":code.load_error_rsn()"
end
test "includes type specs" do
context = eval(do: nil)
assert %{contents: [type]} = Intellisense.get_details("Date.t", 6, context)
assert type =~ "@type t() :: %Date"
assert %{contents: [type]} = Intellisense.get_details(":code.load_error_rsn", 8, context)
assert type =~ "@type load_error_rsn() ::"
# opaque types are listed without internal definition
assert %{contents: [type]} = Intellisense.get_details("MapSet.internal", 10, context)
assert type =~ "@opaque internal(value)\n"
end
test "returns link to online documentation" do
context = eval(do: nil)
@ -1402,6 +1426,14 @@ defmodule Livebook.IntellisenseTest do
assert content =~ ~r"https://hexdocs.pm/elixir/[^/]+/Integer.html#to_string/2"
# test elixir types
assert %{contents: [content]} = Intellisense.get_details("GenServer.on_start", 12, context)
assert content =~ ~r"https://hexdocs.pm/elixir/[^/]+/GenServer.html#t:on_start/0"
# test erlang types
assert %{contents: [content]} = Intellisense.get_details(":code.load_ret", 7, context)
assert content =~ ~r"https://www.erlang.org/doc/man/code.html#type-load_ret"
# test erlang modules on hexdocs
assert %{contents: [content]} = Intellisense.get_details(":telemetry.span", 13, context)
assert content =~ ~r"https://hexdocs.pm/telemetry/[^/]+/telemetry.html#span/3"