From 6c83b910a4b7bcc86c24dc7a40bf56d5246984ec Mon Sep 17 00:00:00 2001 From: Cristine Guadelupe Date: Sat, 30 Sep 2023 15:05:26 +0700 Subject: [PATCH] Remote intellisense - details on hover (#2243) --- lib/livebook/intellisense.ex | 12 +-- .../intellisense/identifier_matcher.ex | 6 +- test/livebook/intellisense_test.exs | 88 ++++++++++++------- test/livebook/remote_intellisense_test.exs | 11 ++- 4 files changed, 76 insertions(+), 41 deletions(-) diff --git a/lib/livebook/intellisense.ex b/lib/livebook/intellisense.ex index 92acfd453..e25668e9b 100644 --- a/lib/livebook/intellisense.ex +++ b/lib/livebook/intellisense.ex @@ -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 [] -> diff --git a/lib/livebook/intellisense/identifier_matcher.ex b/lib/livebook/intellisense/identifier_matcher.ex index 3ecf8ea60..1242d8fae 100644 --- a/lib/livebook/intellisense/identifier_matcher.ex +++ b/lib/livebook/intellisense/identifier_matcher.ex @@ -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) diff --git a/test/livebook/intellisense_test.exs b/test/livebook/intellisense_test.exs index 2910594d8..d485e8c9e 100644 --- a/test/livebook/intellisense_test.exs +++ b/test/livebook/intellisense_test.exs @@ -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("<>", 6, context) - assert nil == Intellisense.get_details("<>", 10, context) + assert nil == Intellisense.get_details("<>", 6, context, node()) + assert nil == Intellisense.get_details("<>", 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 diff --git a/test/livebook/remote_intellisense_test.exs b/test/livebook/remote_intellisense_test.exs index 2fa5d9445..b3ed7701c 100644 --- a/test/livebook/remote_intellisense_test.exs +++ b/test/livebook/remote_intellisense_test.exs @@ -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