Remote intellisense - details on hover (#2243)

This commit is contained in:
Cristine Guadelupe 2023-09-30 15:05:26 +07:00 committed by GitHub
parent 3ca36a6ce7
commit 6c83b910a4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 76 additions and 41 deletions

View file

@ -41,8 +41,8 @@ defmodule Livebook.Intellisense do
%{items: items}
end
def handle_request({:details, line, column}, context, _node) do
get_details(line, column, context)
def handle_request({:details, line, column}, context, node) do
get_details(line, column, context, node)
end
def handle_request({:signature, hint}, context, node) do
@ -409,9 +409,11 @@ defmodule Livebook.Intellisense do
Returns detailed information about an identifier located
in `column` in `line`.
"""
@spec get_details(String.t(), pos_integer(), context()) :: Runtime.details_response() | nil
def get_details(line, column, context) do
%{matches: matches, range: range} = IdentifierMatcher.locate_identifier(line, column, context)
@spec get_details(String.t(), pos_integer(), context(), node()) ::
Runtime.details_response() | nil
def get_details(line, column, context, node) do
%{matches: matches, range: range} =
IdentifierMatcher.locate_identifier(line, column, context, node)
case Enum.filter(matches, &include_in_details?/1) do
[] ->

View file

@ -131,12 +131,12 @@ defmodule Livebook.Intellisense.IdentifierMatcher do
The function returns range of columns where the identifier
is located and a list of matching identifier items.
"""
@spec locate_identifier(String.t(), pos_integer(), Intellisense.context()) ::
@spec locate_identifier(String.t(), pos_integer(), Intellisense.context(), node()) ::
%{
matches: list(identifier_item()),
range: nil | %{from: pos_integer(), to: pos_integer()}
}
def locate_identifier(line, column, intellisense_context) do
def locate_identifier(line, column, intellisense_context, node) do
case Code.Fragment.surround_context(line, {1, column}) do
%{context: context, begin: {_, from}, end: {_, to}} ->
fragment = String.slice(line, 0, to - 1)
@ -146,7 +146,7 @@ defmodule Livebook.Intellisense.IdentifierMatcher do
intellisense_context: intellisense_context,
matcher: @exact_matcher,
type: :locate,
node: node()
node: node
}
matches = context_to_matches(context, ctx)

View file

@ -1350,49 +1350,52 @@ defmodule Livebook.IntellisenseTest do
test "returns nil if there are no matches" do
context = eval(do: nil)
assert nil == Intellisense.get_details("Unknown.unknown()", 2, context)
assert nil == Intellisense.get_details("Unknown.unknown()", 2, context, node())
end
test "returns subject range" do
context = eval(do: nil)
assert %{range: %{from: 1, to: 18}} =
Intellisense.get_details("Integer.to_string(10)", 15, context)
Intellisense.get_details("Integer.to_string(10)", 15, context, node())
assert %{range: %{from: 1, to: 8}} =
Intellisense.get_details("Integer.to_string(10)", 2, context)
Intellisense.get_details("Integer.to_string(10)", 2, context, node())
end
test "does not return duplicate details for functions with default arguments" do
context = eval(do: nil)
assert %{contents: [_]} = Intellisense.get_details("Integer.to_string(10)", 15, context)
assert %{contents: [_]} =
Intellisense.get_details("Integer.to_string(10)", 15, context, node())
end
test "returns details only for exactly matching identifiers" do
context = eval(do: nil)
assert nil == Intellisense.get_details("Enum.ma", 6, context)
assert nil == Intellisense.get_details("Enum.ma", 6, context, node())
end
test "returns full docs" do
context = eval(do: nil)
assert %{contents: [content]} = Intellisense.get_details("Enum.map", 6, context)
assert %{contents: [content]} = Intellisense.get_details("Enum.map", 6, context, node())
assert content =~ "## Examples"
end
test "returns deprecated docs" do
context = eval(do: nil)
assert %{contents: [content | _]} = Intellisense.get_details("Enum.chunk", 6, context)
assert %{contents: [content | _]} =
Intellisense.get_details("Enum.chunk", 6, context, node())
assert content =~ "Use Enum.chunk_every/2 instead"
end
test "returns since docs" do
context = eval(do: nil)
assert %{contents: [content]} = Intellisense.get_details("then", 2, context)
assert %{contents: [content]} = Intellisense.get_details("then", 2, context, node())
assert content =~ "Since 1.12.0"
end
@ -1400,13 +1403,15 @@ defmodule Livebook.IntellisenseTest do
test "returns full Erlang docs" do
context = eval(do: nil)
assert %{contents: [file]} = Intellisense.get_details(":file.read()", 2, context)
assert %{contents: [file]} = Intellisense.get_details(":file.read()", 2, context, node())
assert file =~ "## Performance"
assert %{contents: [file_read]} = Intellisense.get_details(":file.read()", 8, context)
assert %{contents: [file_read]} =
Intellisense.get_details(":file.read()", 8, context, node())
assert file_read =~ "Typical error reasons:"
assert %{contents: [crypto]} = Intellisense.get_details(":crypto", 5, context)
assert %{contents: [crypto]} = Intellisense.get_details(":crypto", 5, context, node())
assert crypto =~ "This module provides a set of cryptographic functions."
end
@ -1414,27 +1419,30 @@ defmodule Livebook.IntellisenseTest do
test "properly renders Erlang signature types list" do
context = eval(do: nil)
assert %{contents: [file_read]} = Intellisense.get_details(":odbc.connect()", 8, context)
assert %{contents: [file_read]} =
Intellisense.get_details(":odbc.connect()", 8, context, node())
assert file_read =~ "Ref = connection_reference()"
end
test "properly parses unicode" do
context = eval(do: nil)
assert nil == Intellisense.get_details("msg = '🍵'", 8, context)
assert nil == Intellisense.get_details("msg = '🍵'", 8, context, node())
end
test "handles operators" do
context = eval(do: nil)
assert %{contents: [match_op]} = Intellisense.get_details("x = 1", 3, context)
assert %{contents: [match_op]} = Intellisense.get_details("x = 1", 3, context, node())
assert match_op =~ "Match operator."
end
test "handles local calls" do
context = eval(do: nil)
assert %{contents: [to_string_fn]} = Intellisense.get_details("to_string(1)", 3, context)
assert %{contents: [to_string_fn]} =
Intellisense.get_details("to_string(1)", 3, context, node())
assert to_string_fn =~ "Converts the argument to a string"
end
@ -1442,73 +1450,91 @@ defmodule Livebook.IntellisenseTest do
test "returns nil for bitstring modifiers" do
context = eval(do: nil)
assert nil == Intellisense.get_details("<<x :: integer>>", 6, context)
assert nil == Intellisense.get_details("<<x :: integer>>", 10, context)
assert nil == Intellisense.get_details("<<x :: integer>>", 6, context, node())
assert nil == Intellisense.get_details("<<x :: integer>>", 10, context, node())
end
test "includes full module name in the docs" do
context = eval(do: nil)
assert %{contents: [date_range]} = Intellisense.get_details("Date.Range", 8, context)
assert %{contents: [date_range]} =
Intellisense.get_details("Date.Range", 8, context, node())
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 %{contents: [type]} = Intellisense.get_details("Date.t", 6, context, node())
assert type =~ "Date.t()"
assert %{contents: [type]} = Intellisense.get_details(":code.load_error_rsn", 8, context)
assert %{contents: [type]} =
Intellisense.get_details(":code.load_error_rsn", 8, context, node())
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 %{contents: [type]} = Intellisense.get_details("Date.t", 6, context, node())
assert type =~ "@type t() :: %Date"
assert %{contents: [type]} = Intellisense.get_details(":code.load_error_rsn", 8, context)
assert %{contents: [type]} =
Intellisense.get_details(":code.load_error_rsn", 8, context, node())
assert type =~ "@type load_error_rsn() ::"
# opaque types are listed without internal definition
assert %{contents: [type]} = Intellisense.get_details("MapSet.internal", 10, context)
assert %{contents: [type]} =
Intellisense.get_details("MapSet.internal", 10, context, node())
assert type =~ "@opaque internal(value)\n"
end
test "returns link to online documentation" do
context = eval(do: nil)
assert %{contents: [content]} = Intellisense.get_details("Integer", 1, context)
assert %{contents: [content]} = Intellisense.get_details("Integer", 1, context, node())
assert content =~ ~r"https://hexdocs.pm/elixir/[^/]+/Integer.html"
assert %{contents: [content]} =
Intellisense.get_details("Integer.to_string(10)", 15, context)
Intellisense.get_details("Integer.to_string(10)", 15, context, node())
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 %{contents: [content]} =
Intellisense.get_details("GenServer.on_start", 12, context, node())
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 %{contents: [content]} =
Intellisense.get_details(":code.load_ret", 7, context, node())
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 %{contents: [content]} =
Intellisense.get_details(":telemetry.span", 13, context, node())
assert content =~ ~r"https://hexdocs.pm/telemetry/[^/]+/telemetry.html#span/3"
# test erlang applications
assert %{contents: [content]} = Intellisense.get_details(":code", 3, context)
assert %{contents: [content]} = Intellisense.get_details(":code", 3, context, node())
assert content =~ ~r"https://www.erlang.org/doc/man/code.html"
assert %{contents: [content]} = Intellisense.get_details(":code.load_binary", 10, context)
assert %{contents: [content]} =
Intellisense.get_details(":code.load_binary", 10, context, node())
assert content =~ ~r"https://www.erlang.org/doc/man/code.html#load_binary-3"
# test erlang modules
assert %{contents: [content]} = Intellisense.get_details(":atomics.new", 11, context)
assert %{contents: [content]} =
Intellisense.get_details(":atomics.new", 11, context, node())
assert content =~ ~r"https://www.erlang.org/doc/man/atomics.html#new-2"
end
end

View file

@ -32,7 +32,7 @@ defmodule Livebook.RemoteIntellisenseTest do
defmodule Elixir.RemoteModule do
@compile {:autoload, false}
@moduledoc """
Remote module docs
RemoteModule module docs
"""
@doc """
@ -73,7 +73,7 @@ defmodule Livebook.RemoteIntellisenseTest do
label: "RemoteModule",
kind: :module,
detail: "module",
documentation: "Remote module docs",
documentation: "RemoteModule module docs",
insert_text: "RemoteModule"
} in Intellisense.get_completion_items("RemoteModule", context, node)
end
@ -103,5 +103,12 @@ defmodule Livebook.RemoteIntellisenseTest do
}
] = Intellisense.get_completion_items(":mnesia.all", context, node)
end
test "get details", %{node: node} do
context = eval(do: nil)
assert %{contents: [content]} = Intellisense.get_details("RemoteModule", 6, context, node)
assert content =~ "RemoteModule module docs"
end
end
end