mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-09-19 03:14:36 +08:00
Add on-hover details for types (#1974)
This commit is contained in:
parent
b2e21aeac2
commit
7691324a4e
4 changed files with 118 additions and 9 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Add table
Reference in a new issue