mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-10-26 21:36:02 +08:00
Remote intellisense - details on hover (#2243)
This commit is contained in:
parent
3ca36a6ce7
commit
6c83b910a4
4 changed files with 76 additions and 41 deletions
|
|
@ -41,8 +41,8 @@ defmodule Livebook.Intellisense do
|
||||||
%{items: items}
|
%{items: items}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_request({:details, line, column}, context, _node) do
|
def handle_request({:details, line, column}, context, node) do
|
||||||
get_details(line, column, context)
|
get_details(line, column, context, node)
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_request({:signature, hint}, context, node) do
|
def handle_request({:signature, hint}, context, node) do
|
||||||
|
|
@ -409,9 +409,11 @@ defmodule Livebook.Intellisense do
|
||||||
Returns detailed information about an identifier located
|
Returns detailed information about an identifier located
|
||||||
in `column` in `line`.
|
in `column` in `line`.
|
||||||
"""
|
"""
|
||||||
@spec get_details(String.t(), pos_integer(), context()) :: Runtime.details_response() | nil
|
@spec get_details(String.t(), pos_integer(), context(), node()) ::
|
||||||
def get_details(line, column, context) do
|
Runtime.details_response() | nil
|
||||||
%{matches: matches, range: range} = IdentifierMatcher.locate_identifier(line, column, context)
|
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
|
case Enum.filter(matches, &include_in_details?/1) do
|
||||||
[] ->
|
[] ->
|
||||||
|
|
|
||||||
|
|
@ -131,12 +131,12 @@ defmodule Livebook.Intellisense.IdentifierMatcher do
|
||||||
The function returns range of columns where the identifier
|
The function returns range of columns where the identifier
|
||||||
is located and a list of matching identifier items.
|
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()),
|
matches: list(identifier_item()),
|
||||||
range: nil | %{from: pos_integer(), to: pos_integer()}
|
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
|
case Code.Fragment.surround_context(line, {1, column}) do
|
||||||
%{context: context, begin: {_, from}, end: {_, to}} ->
|
%{context: context, begin: {_, from}, end: {_, to}} ->
|
||||||
fragment = String.slice(line, 0, to - 1)
|
fragment = String.slice(line, 0, to - 1)
|
||||||
|
|
@ -146,7 +146,7 @@ defmodule Livebook.Intellisense.IdentifierMatcher do
|
||||||
intellisense_context: intellisense_context,
|
intellisense_context: intellisense_context,
|
||||||
matcher: @exact_matcher,
|
matcher: @exact_matcher,
|
||||||
type: :locate,
|
type: :locate,
|
||||||
node: node()
|
node: node
|
||||||
}
|
}
|
||||||
|
|
||||||
matches = context_to_matches(context, ctx)
|
matches = context_to_matches(context, ctx)
|
||||||
|
|
|
||||||
|
|
@ -1350,49 +1350,52 @@ defmodule Livebook.IntellisenseTest do
|
||||||
test "returns nil if there are no matches" do
|
test "returns nil if there are no matches" do
|
||||||
context = eval(do: nil)
|
context = eval(do: nil)
|
||||||
|
|
||||||
assert nil == Intellisense.get_details("Unknown.unknown()", 2, context)
|
assert nil == Intellisense.get_details("Unknown.unknown()", 2, context, node())
|
||||||
end
|
end
|
||||||
|
|
||||||
test "returns subject range" do
|
test "returns subject range" do
|
||||||
context = eval(do: nil)
|
context = eval(do: nil)
|
||||||
|
|
||||||
assert %{range: %{from: 1, to: 18}} =
|
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}} =
|
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
|
end
|
||||||
|
|
||||||
test "does not return duplicate details for functions with default arguments" do
|
test "does not return duplicate details for functions with default arguments" do
|
||||||
context = eval(do: nil)
|
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
|
end
|
||||||
|
|
||||||
test "returns details only for exactly matching identifiers" do
|
test "returns details only for exactly matching identifiers" do
|
||||||
context = eval(do: nil)
|
context = eval(do: nil)
|
||||||
|
|
||||||
assert nil == Intellisense.get_details("Enum.ma", 6, context)
|
assert nil == Intellisense.get_details("Enum.ma", 6, context, node())
|
||||||
end
|
end
|
||||||
|
|
||||||
test "returns full docs" do
|
test "returns full docs" do
|
||||||
context = eval(do: nil)
|
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"
|
assert content =~ "## Examples"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "returns deprecated docs" do
|
test "returns deprecated docs" do
|
||||||
context = eval(do: nil)
|
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"
|
assert content =~ "Use Enum.chunk_every/2 instead"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "returns since docs" do
|
test "returns since docs" do
|
||||||
context = eval(do: nil)
|
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"
|
assert content =~ "Since 1.12.0"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -1400,13 +1403,15 @@ defmodule Livebook.IntellisenseTest do
|
||||||
test "returns full Erlang docs" do
|
test "returns full Erlang docs" do
|
||||||
context = eval(do: nil)
|
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 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 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."
|
assert crypto =~ "This module provides a set of cryptographic functions."
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -1414,27 +1419,30 @@ defmodule Livebook.IntellisenseTest do
|
||||||
test "properly renders Erlang signature types list" do
|
test "properly renders Erlang signature types list" do
|
||||||
context = eval(do: nil)
|
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()"
|
assert file_read =~ "Ref = connection_reference()"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "properly parses unicode" do
|
test "properly parses unicode" do
|
||||||
context = eval(do: nil)
|
context = eval(do: nil)
|
||||||
|
|
||||||
assert nil == Intellisense.get_details("msg = '🍵'", 8, context)
|
assert nil == Intellisense.get_details("msg = '🍵'", 8, context, node())
|
||||||
end
|
end
|
||||||
|
|
||||||
test "handles operators" do
|
test "handles operators" do
|
||||||
context = eval(do: nil)
|
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."
|
assert match_op =~ "Match operator."
|
||||||
end
|
end
|
||||||
|
|
||||||
test "handles local calls" do
|
test "handles local calls" do
|
||||||
context = eval(do: nil)
|
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"
|
assert to_string_fn =~ "Converts the argument to a string"
|
||||||
end
|
end
|
||||||
|
|
@ -1442,73 +1450,91 @@ defmodule Livebook.IntellisenseTest do
|
||||||
test "returns nil for bitstring modifiers" do
|
test "returns nil for bitstring modifiers" do
|
||||||
context = eval(do: nil)
|
context = eval(do: nil)
|
||||||
|
|
||||||
assert nil == Intellisense.get_details("<<x :: integer>>", 6, context)
|
assert nil == Intellisense.get_details("<<x :: integer>>", 6, context, node())
|
||||||
assert nil == Intellisense.get_details("<<x :: integer>>", 10, context)
|
assert nil == Intellisense.get_details("<<x :: integer>>", 10, context, node())
|
||||||
end
|
end
|
||||||
|
|
||||||
test "includes full module name in the docs" do
|
test "includes full module name in the docs" do
|
||||||
context = eval(do: nil)
|
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"
|
assert date_range =~ "Date.Range"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "returns module-prepended type signatures" do
|
test "returns module-prepended type signatures" do
|
||||||
context = eval(do: nil)
|
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 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()"
|
assert type =~ ":code.load_error_rsn()"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "includes type specs" do
|
test "includes type specs" do
|
||||||
context = eval(do: nil)
|
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 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() ::"
|
assert type =~ "@type load_error_rsn() ::"
|
||||||
|
|
||||||
# opaque types are listed without internal definition
|
# 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"
|
assert type =~ "@opaque internal(value)\n"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "returns link to online documentation" do
|
test "returns link to online documentation" do
|
||||||
context = eval(do: nil)
|
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 content =~ ~r"https://hexdocs.pm/elixir/[^/]+/Integer.html"
|
||||||
|
|
||||||
assert %{contents: [content]} =
|
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"
|
assert content =~ ~r"https://hexdocs.pm/elixir/[^/]+/Integer.html#to_string/2"
|
||||||
|
|
||||||
# test elixir types
|
# 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"
|
assert content =~ ~r"https://hexdocs.pm/elixir/[^/]+/GenServer.html#t:on_start/0"
|
||||||
|
|
||||||
# test erlang types
|
# 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"
|
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, node())
|
||||||
|
|
||||||
assert content =~ ~r"https://hexdocs.pm/telemetry/[^/]+/telemetry.html#span/3"
|
assert content =~ ~r"https://hexdocs.pm/telemetry/[^/]+/telemetry.html#span/3"
|
||||||
|
|
||||||
# test erlang applications
|
# 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 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"
|
assert content =~ ~r"https://www.erlang.org/doc/man/code.html#load_binary-3"
|
||||||
|
|
||||||
# test erlang modules
|
# 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"
|
assert content =~ ~r"https://www.erlang.org/doc/man/atomics.html#new-2"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ defmodule Livebook.RemoteIntellisenseTest do
|
||||||
defmodule Elixir.RemoteModule do
|
defmodule Elixir.RemoteModule do
|
||||||
@compile {:autoload, false}
|
@compile {:autoload, false}
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Remote module docs
|
RemoteModule module docs
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
|
@ -73,7 +73,7 @@ defmodule Livebook.RemoteIntellisenseTest do
|
||||||
label: "RemoteModule",
|
label: "RemoteModule",
|
||||||
kind: :module,
|
kind: :module,
|
||||||
detail: "module",
|
detail: "module",
|
||||||
documentation: "Remote module docs",
|
documentation: "RemoteModule module docs",
|
||||||
insert_text: "RemoteModule"
|
insert_text: "RemoteModule"
|
||||||
} in Intellisense.get_completion_items("RemoteModule", context, node)
|
} in Intellisense.get_completion_items("RemoteModule", context, node)
|
||||||
end
|
end
|
||||||
|
|
@ -103,5 +103,12 @@ defmodule Livebook.RemoteIntellisenseTest do
|
||||||
}
|
}
|
||||||
] = Intellisense.get_completion_items(":mnesia.all", context, node)
|
] = Intellisense.get_completion_items(":mnesia.all", context, node)
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue