mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-09-26 06:44:49 +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([
|
join_with_divider([
|
||||||
format_signatures(signatures, module) |> code(),
|
format_signatures(signatures, module) |> code(),
|
||||||
join_with_middle_dot([
|
join_with_middle_dot([
|
||||||
format_docs_link(module, name, arity),
|
format_docs_link(module, {:function, name, arity}),
|
||||||
format_meta(:since, meta)
|
format_meta(:since, meta)
|
||||||
]),
|
]),
|
||||||
format_meta(:deprecated, meta),
|
format_meta(:deprecated, meta),
|
||||||
|
@ -467,9 +467,18 @@ defmodule Livebook.Intellisense do
|
||||||
])
|
])
|
||||||
end
|
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([
|
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)
|
format_documentation(documentation, :all)
|
||||||
])
|
])
|
||||||
end
|
end
|
||||||
|
@ -506,7 +515,7 @@ defmodule Livebook.Intellisense do
|
||||||
"""
|
"""
|
||||||
end
|
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)
|
app = Application.get_application(module)
|
||||||
|
|
||||||
module_name =
|
module_name =
|
||||||
|
@ -524,12 +533,24 @@ defmodule Livebook.Intellisense do
|
||||||
|
|
||||||
cond do
|
cond do
|
||||||
is_otp? ->
|
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}"
|
url = "https://www.erlang.org/doc/man/#{module_name}.html#{hash}"
|
||||||
"[View on Erlang Docs](#{url})"
|
"[View on Erlang Docs](#{url})"
|
||||||
|
|
||||||
vsn = app && Application.spec(app, :vsn) ->
|
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}"
|
url = "https://hexdocs.pm/#{app}/#{vsn}/#{module_name}.html#{hash}"
|
||||||
"[View on Hexdocs](#{url})"
|
"[View on Hexdocs](#{url})"
|
||||||
|
|
||||||
|
@ -551,6 +572,13 @@ defmodule Livebook.Intellisense do
|
||||||
end
|
end
|
||||||
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
|
defp format_meta(:deprecated, %{deprecated: deprecated}) do
|
||||||
"**Deprecated**. " <> deprecated
|
"**Deprecated**. " <> deprecated
|
||||||
end
|
end
|
||||||
|
@ -582,6 +610,27 @@ defmodule Livebook.Intellisense do
|
||||||
end
|
end
|
||||||
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(doc, variant)
|
||||||
|
|
||||||
defp format_documentation(nil, _variant) do
|
defp format_documentation(nil, _variant) do
|
||||||
|
|
|
@ -12,6 +12,7 @@ defmodule Livebook.Intellisense.Docs do
|
||||||
documentation: documentation(),
|
documentation: documentation(),
|
||||||
signatures: list(signature()),
|
signatures: list(signature()),
|
||||||
specs: list(spec()),
|
specs: list(spec()),
|
||||||
|
type_spec: type_spec(),
|
||||||
meta: meta()
|
meta: meta()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,6 +29,13 @@ defmodule Livebook.Intellisense.Docs do
|
||||||
"""
|
"""
|
||||||
@type spec :: term()
|
@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 """
|
@doc """
|
||||||
Fetches documentation for the given module if available.
|
Fetches documentation for the given module if available.
|
||||||
"""
|
"""
|
||||||
|
@ -80,6 +88,17 @@ defmodule Livebook.Intellisense.Docs do
|
||||||
_ -> %{}
|
_ -> %{}
|
||||||
end
|
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
|
case Code.fetch_docs(module) do
|
||||||
{:docs_v1, _, _, format, _, _, docs} ->
|
{:docs_v1, _, _, format, _, _, docs} ->
|
||||||
for {{kind, name, base_arity}, _line, signatures, doc, meta} <- docs,
|
for {{kind, name, base_arity}, _line, signatures, doc, meta} <- docs,
|
||||||
|
@ -95,6 +114,7 @@ defmodule Livebook.Intellisense.Docs do
|
||||||
documentation: documentation(doc, format),
|
documentation: documentation(doc, format),
|
||||||
signatures: signatures,
|
signatures: signatures,
|
||||||
specs: Map.get(specs, {name, base_arity}, []),
|
specs: Map.get(specs, {name, base_arity}, []),
|
||||||
|
type_spec: Map.get(type_specs, {name, base_arity}, nil),
|
||||||
meta: meta
|
meta: meta
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -62,7 +62,8 @@ defmodule Livebook.Intellisense.IdentifierMatcher do
|
||||||
module: module(),
|
module: module(),
|
||||||
name: name(),
|
name: name(),
|
||||||
arity: arity(),
|
arity: arity(),
|
||||||
documentation: Docs.documentation()
|
documentation: Docs.documentation(),
|
||||||
|
type_spec: Docs.type_spec()
|
||||||
}
|
}
|
||||||
| %{
|
| %{
|
||||||
kind: :module_attribute,
|
kind: :module_attribute,
|
||||||
|
@ -710,11 +711,18 @@ defmodule Livebook.Intellisense.IdentifierMatcher do
|
||||||
|
|
||||||
Enum.map(matching_types, fn {name, arity} ->
|
Enum.map(matching_types, fn {name, arity} ->
|
||||||
doc_item =
|
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
|
doc_item.name == name && doc_item.arity == arity
|
||||||
end)
|
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)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1391,6 +1391,30 @@ defmodule Livebook.IntellisenseTest do
|
||||||
assert date_range =~ "Date.Range"
|
assert date_range =~ "Date.Range"
|
||||||
end
|
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
|
test "returns link to online documentation" do
|
||||||
context = eval(do: nil)
|
context = eval(do: nil)
|
||||||
|
|
||||||
|
@ -1402,6 +1426,14 @@ defmodule Livebook.IntellisenseTest do
|
||||||
|
|
||||||
assert content =~ ~r"https://hexdocs.pm/elixir/[^/]+/Integer.html#to_string/2"
|
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
|
# test erlang modules on hexdocs
|
||||||
assert %{contents: [content]} = Intellisense.get_details(":telemetry.span", 13, context)
|
assert %{contents: [content]} = Intellisense.get_details(":telemetry.span", 13, context)
|
||||||
assert content =~ ~r"https://hexdocs.pm/telemetry/[^/]+/telemetry.html#span/3"
|
assert content =~ ~r"https://hexdocs.pm/telemetry/[^/]+/telemetry.html#span/3"
|
||||||
|
|
Loading…
Add table
Reference in a new issue