diff --git a/bin/nextls b/bin/nextls deleted file mode 100755 index 563dd49f..00000000 --- a/bin/nextls +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env elixir - -Node.start(:"next-ls-#{System.system_time()}", :shortnames) - -System.no_halt(true) - -Logger.configure(level: :none) - -Mix.start() -Mix.shell(Mix.Shell.Process) - -default_version = "0.7.1" # x-release-please-version - -Mix.install([{:next_ls, System.get_env("NEXTLS_VERSION", default_version)}]) - -Logger.configure(level: :info) - -Application.ensure_all_started(:next_ls) diff --git a/lib/next_ls.ex b/lib/next_ls.ex index 5df083b7..4c772b50 100644 --- a/lib/next_ls.ex +++ b/lib/next_ls.ex @@ -19,6 +19,7 @@ defmodule NextLS do alias GenLSP.Requests.TextDocumentDefinition alias GenLSP.Requests.TextDocumentDocumentSymbol alias GenLSP.Requests.TextDocumentFormatting + alias GenLSP.Requests.TextDocumentHover alias GenLSP.Requests.TextDocumentReferences alias GenLSP.Requests.WorkspaceSymbol alias GenLSP.Structures.DidChangeWatchedFilesParams @@ -110,6 +111,7 @@ defmodule NextLS do change: TextDocumentSyncKind.full() }, document_formatting_provider: true, + hover_provider: true, workspace_symbol_provider: true, document_symbol_provider: true, references_provider: true, @@ -248,6 +250,102 @@ defmodule NextLS do {:reply, locations, lsp} end + def handle_request(%TextDocumentHover{params: %{position: position, text_document: %{uri: uri}}}, lsp) do + file = URI.parse(uri).path + line = position.line + 1 + col = position.character + 1 + + select = ~wa + + reference_query = ~Q""" + SELECT :select + FROM "references" refs + WHERE refs.file = ? + AND ? BETWEEN refs.start_line AND refs.end_line + AND ? BETWEEN refs.start_column AND refs.end_column + ORDER BY refs.id ASC + LIMIT 1 + """ + + locations = + dispatch(lsp.assigns.registry, :databases, fn databases -> + Enum.flat_map(databases, fn {database, _} -> + DB.query(database, reference_query, args: [file, line, col], select: select) + end) + end) + + resp = + case locations do + [reference] -> + mod = + if reference.module == String.downcase(reference.module) do + String.to_existing_atom(reference.module) + else + Module.concat([reference.module]) + end + + result = + dispatch(lsp.assigns.registry, :runtimes, fn entries -> + [result] = + for {runtime, %{uri: wuri}} <- entries, String.starts_with?(uri, wuri) do + Runtime.call(runtime, {Code, :fetch_docs, [mod]}) + end + + result + end) + + value = + with {:ok, {:docs_v1, _, _lang, content_type, %{"en" => mod_doc}, _, fdocs}} <- result do + case reference.type do + "alias" -> + """ + ## #{reference.module} + + #{NextLS.HoverHelpers.to_markdown(content_type, mod_doc)} + """ + + "function" -> + {_, _, _, doc, _} = + Enum.find(fdocs, fn {{type, name, _a}, _, _, _doc, _} -> + type in [:function, :macro] and to_string(name) == reference.identifier + end) + + case doc do + %{"en" => fdoc} -> + """ + ## #{Macro.to_string(mod)}.#{reference.identifier}/#{reference.arity} + + #{NextLS.HoverHelpers.to_markdown(content_type, fdoc)} + """ + + _ -> + nil + end + end + else + _ -> nil + end + + with value when is_binary(value) <- value do + %GenLSP.Structures.Hover{ + contents: %GenLSP.Structures.MarkupContent{ + kind: GenLSP.Enumerations.MarkupKind.markdown(), + value: String.trim(value) + }, + range: %Range{ + start: %Position{line: reference.start_line - 1, character: reference.start_column - 1}, + end: %Position{line: reference.end_line - 1, character: reference.end_column - 1} + } + } + end + + _ -> + nil + end + + {:reply, resp, lsp} + end + def handle_request(%WorkspaceSymbol{params: %{query: query}}, lsp) do case_sensitive? = String.downcase(query) != query diff --git a/lib/next_ls/db.ex b/lib/next_ls/db.ex index 5f88def8..90ab1132 100644 --- a/lib/next_ls/db.ex +++ b/lib/next_ls/db.ex @@ -11,7 +11,7 @@ defmodule NextLS.DB do end @spec query(pid(), query(), list()) :: list() - def query(server, query, args \\ []), do: GenServer.call(server, {:query, query, args}, :infinity) + def query(server, query, opts \\ []), do: GenServer.call(server, {:query, query, opts}, :infinity) @spec insert_symbol(pid(), map()) :: :ok def insert_symbol(server, payload), do: GenServer.cast(server, {:insert_symbol, payload}) @@ -43,10 +43,26 @@ defmodule NextLS.DB do }} end - def handle_call({:query, query, args}, _from, %{conn: conn} = s) do + def handle_call({:query, query, args_or_opts}, _from, %{conn: conn} = s) do {:message_queue_len, count} = Process.info(self(), :message_queue_len) NextLS.DB.Activity.update(s.activity, count) - rows = __query__({conn, s.logger}, query, args) + opts = if Keyword.keyword?(args_or_opts), do: args_or_opts, else: [args: args_or_opts] + + query = + if opts[:select] do + String.replace(query, ":select", Enum.map_join(opts[:select], ", ", &to_string/1)) + else + query + end + + rows = + for row <- __query__({conn, s.logger}, query, opts[:args] || []) do + if opts[:select] do + opts[:select] |> Enum.zip(row) |> Map.new() + else + row + end + end {:reply, rows, s} end @@ -134,23 +150,25 @@ defmodule NextLS.DB do source: source } = reference - line = meta[:line] || 1 - col = meta[:column] || 0 + if (meta[:line] && meta[:column]) || (reference[:range][:start] && reference[:range][:stop]) do + line = meta[:line] || 1 + col = meta[:column] || 0 - {start_line, start_column} = reference[:range][:start] || {line, col} + {start_line, start_column} = reference[:range][:start] || {line, col} - {end_line, end_column} = - reference[:range][:stop] || - {line, col + String.length(identifier |> to_string() |> String.replace("Elixir.", ""))} + {end_line, end_column} = + reference[:range][:stop] || + {line, col + String.length(identifier |> to_string() |> String.replace("Elixir.", "")) - 1} - __query__( - {conn, s.logger}, - ~Q""" - INSERT INTO 'references' (identifier, arity, file, type, module, start_line, start_column, end_line, end_column, source) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?); - """, - [identifier, reference[:arity], file, type, module, start_line, start_column, end_line, end_column, source] - ) + __query__( + {conn, s.logger}, + ~Q""" + INSERT INTO 'references' (identifier, arity, file, type, module, start_line, start_column, end_line, end_column, source) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?); + """, + [identifier, reference[:arity], file, type, module, start_line, start_column, end_line, end_column, source] + ) + end {:noreply, s} end diff --git a/lib/next_ls/ast_helpers.ex b/lib/next_ls/helpers/ast_helpers.ex similarity index 100% rename from lib/next_ls/ast_helpers.ex rename to lib/next_ls/helpers/ast_helpers.ex diff --git a/lib/next_ls/helpers/hover_helpers.ex b/lib/next_ls/helpers/hover_helpers.ex new file mode 100644 index 00000000..9c74bf2f --- /dev/null +++ b/lib/next_ls/helpers/hover_helpers.ex @@ -0,0 +1,56 @@ +defmodule NextLS.HoverHelpers do + @moduledoc false + + @spec to_markdown(String.t(), String.t() | list()) :: String.t() + def to_markdown(type, docs) + def to_markdown("text/markdown", docs), do: docs + + def to_markdown("application/erlang+html" = type, [{:p, _, children} | rest]) do + String.trim(to_markdown(type, children) <> "\n\n" <> to_markdown(type, rest)) + end + + def to_markdown("application/erlang+html" = type, [{:div, attrs, children} | rest]) do + prefix = + if attrs[:class] in ~w do + "> " + else + "" + end + + prefix <> to_markdown(type, children) <> "\n\n" <> to_markdown(type, rest) + end + + def to_markdown("application/erlang+html" = type, [{:a, attrs, children} | rest]) do + space = if List.last(children) == " ", do: " ", else: "" + + "[#{String.trim(to_markdown(type, children))}](#{attrs[:href]})" <> space <> to_markdown(type, rest) + end + + def to_markdown("application/erlang+html" = type, [doc | rest]) when is_binary(doc) do + doc <> to_markdown(type, rest) + end + + def to_markdown("application/erlang+html" = type, [{:pre, _, [{:code, _, children}]} | rest]) do + "```erlang\n#{to_markdown(type, children)}\n```\n\n" <> to_markdown(type, rest) + end + + def to_markdown("application/erlang+html" = type, [{:ul, _, lis} | rest]) do + "#{to_markdown(type, lis)}\n" <> to_markdown(type, rest) + end + + def to_markdown("application/erlang+html" = type, [{:li, _, children} | rest]) do + "* #{to_markdown(type, children)}\n" <> to_markdown(type, rest) + end + + def to_markdown("application/erlang+html" = type, [{:code, _, bins} | rest]) do + "`#{IO.iodata_to_binary(bins)}`" <> to_markdown(type, rest) + end + + def to_markdown("application/erlang+html" = type, [{:em, _, bins} | rest]) do + "_#{IO.iodata_to_binary(bins)}_" <> to_markdown(type, rest) + end + + def to_markdown("application/erlang+html", []) do + "" + end +end diff --git a/lib/next_ls/runtime/sidecar.ex b/lib/next_ls/runtime/sidecar.ex index b725c110..805883ba 100644 --- a/lib/next_ls/runtime/sidecar.ex +++ b/lib/next_ls/runtime/sidecar.ex @@ -16,6 +16,12 @@ defmodule NextLS.Runtime.Sidecar do {:ok, %{db: db}} end + def handle_info({:tracer, :dbg, term}, state) do + dbg(term) + + {:noreply, state} + end + def handle_info({:tracer, payload}, state) do attributes = Attributes.get_module_attributes(payload.file, payload.module) payload = Map.put_new(payload, :symbols, attributes) diff --git a/priv/monkey/_next_ls_private_compiler.ex b/priv/monkey/_next_ls_private_compiler.ex index eb353091..d7bc7b0f 100644 --- a/priv/monkey/_next_ls_private_compiler.ex +++ b/priv/monkey/_next_ls_private_compiler.ex @@ -82,8 +82,9 @@ defmodule NextLSPrivate.Tracer do :ok end - def trace({:alias_reference, meta, module}, env) do + def trace({:alias_reference, meta, module} = term, env) do parent = parent_pid() + # Process.send(parent, {:tracer, :dbg, {term, env.file}}, []) alias_map = Map.new(env.aliases, fn {alias, mod} -> {mod, alias} end) @@ -176,6 +177,7 @@ defmodule NextLSPrivate.Tracer do def trace({:on_module, bytecode, _}, env) do parent = parent_pid() + # Process.send(parent, {:tracer, :dbg, {:on_module, env}}, []) defs = Module.definitions_in(env.module) diff --git a/test/next_ls/dependency_test.exs b/test/next_ls/dependency_test.exs index b0b85282..f5965645 100644 --- a/test/next_ls/dependency_test.exs +++ b/test/next_ls/dependency_test.exs @@ -160,7 +160,7 @@ defmodule NextLS.DependencyTest do 4, [ %{ - "range" => %{"start" => %{"character" => 8, "line" => 7}, "end" => %{"character" => 11, "line" => 7}}, + "range" => %{"start" => %{"character" => 8, "line" => 7}, "end" => %{"character" => 10, "line" => 7}}, "uri" => uri } ] @@ -190,11 +190,11 @@ defmodule NextLS.DependencyTest do 4, [ %{ - "range" => %{"start" => %{"character" => 4, "line" => 3}, "end" => %{"character" => 7, "line" => 3}}, + "range" => %{"start" => %{"character" => 4, "line" => 3}, "end" => %{"character" => 6, "line" => 3}}, "uri" => uri }, %{ - "range" => %{"start" => %{"character" => 4, "line" => 7}, "end" => %{"character" => 7, "line" => 7}}, + "range" => %{"start" => %{"character" => 4, "line" => 7}, "end" => %{"character" => 6, "line" => 7}}, "uri" => uri } ] @@ -229,19 +229,11 @@ defmodule NextLS.DependencyTest do 4, [ %{ - "range" => %{"end" => %{"character" => 15, "line" => 1}, "start" => %{"character" => 6, "line" => 1}}, + "range" => %{"end" => %{"character" => 14, "line" => 1}, "start" => %{"character" => 6, "line" => 1}}, "uri" => uri }, %{ - "range" => %{"end" => %{"character" => 8, "line" => 1}, "start" => %{"character" => 0, "line" => 1}}, - "uri" => uri - }, - %{ - "range" => %{"end" => %{"character" => 8, "line" => 1}, "start" => %{"character" => 0, "line" => 1}}, - "uri" => uri - }, - %{ - "range" => %{"end" => %{"character" => 13, "line" => 8}, "start" => %{"character" => 4, "line" => 8}}, + "range" => %{"end" => %{"character" => 12, "line" => 8}, "start" => %{"character" => 4, "line" => 8}}, "uri" => uri } ] diff --git a/test/next_ls/ast_helpers_test.exs b/test/next_ls/helpers/ast_helpers_test.exs similarity index 100% rename from test/next_ls/ast_helpers_test.exs rename to test/next_ls/helpers/ast_helpers_test.exs diff --git a/test/next_ls/helpers/hover_helpers_test.exs b/test/next_ls/helpers/hover_helpers_test.exs new file mode 100644 index 00000000..c44f68e8 --- /dev/null +++ b/test/next_ls/helpers/hover_helpers_test.exs @@ -0,0 +1,204 @@ +defmodule NextLS.HoverHelpersTest do + use ExUnit.Case, async: true + + alias NextLS.HoverHelpers + + describe "converts erlang html format to markdown" do + test "some divs and p and code" do + html = [ + {:p, [], + [ + "Suspends the process calling this function for ", + {:code, [], ["Time"]}, + " milliseconds and then returns ", + {:code, [], ["ok"]}, + ", or suspends the process forever if ", + {:code, [], ["Time"]}, + " is the atom ", + {:code, [], ["infinity"]}, + ". Naturally, this function does ", + {:em, [], ["not"]}, + " return immediately." + ]}, + {:div, [class: "note"], + [ + {:p, [], + [ + "Before OTP 25, ", + {:code, [], ["timer:sleep/1"]}, + " did not accept integer timeout values greater than ", + {:code, [], ["16#ffffffff"]}, + ", that is, ", + {:code, [], ["2^32-1"]}, + ". Since OTP 25, arbitrarily high integer values are accepted." + ]} + ]} + ] + + actual = HoverHelpers.to_markdown("application/erlang+html", html) + + assert actual == + String.trim(""" + Suspends the process calling this function for `Time` milliseconds and then returns `ok`, or suspends the process forever if `Time` is the atom `infinity`. Naturally, this function does _not_ return immediately. + + > Before OTP 25, `timer:sleep/1` did not accept integer timeout values greater than `16#ffffffff`, that is, `2^32-1`. Since OTP 25, arbitrarily high integer values are accepted. + """) + end + + test "some p and a and code" do + html = [ + {:p, [], + [ + "The same as ", + {:a, + [ + href: "erts:erlang#atom_to_binary/2", + rel: "https://erlang.org/doc/link/seemfa" + ], [{:code, [], ["atom_to_binary"]}, " "]}, + {:code, [], ["(Atom, utf8)"]}, + "." + ]} + ] + + actual = HoverHelpers.to_markdown("application/erlang+html", html) + + assert actual == + String.trim(""" + The same as [`atom_to_binary`](erts:erlang#atom_to_binary/2) `(Atom, utf8)`. + """) + end + + test "some code" do + html = [ + {:p, [], + [ + "Extracts the part of the binary described by ", + {:code, [], ["PosLen"]}, + "." + ]}, + {:p, [], ["Negative length can be used to extract bytes at the end of a binary, for example:"]}, + {:pre, [], + [ + {:code, [], + ["1> Bin = <<1,2,3,4,5,6,7,8,9,10>>.\n2> binary_part(Bin,{byte_size(Bin), -5}).\n<<6,7,8,9,10>>"]} + ]}, + {:p, [], + [ + "Failure: ", + {:code, [], ["badarg"]}, + " if ", + {:code, [], ["PosLen"]}, + " in any way references outside the binary." + ]}, + {:p, [], [{:code, [], ["Start"]}, " is zero-based, that is:"]}, + {:pre, [], [{:code, [], ["1> Bin = <<1,2,3>>\n2> binary_part(Bin,{0,2}).\n<<1,2>>"]}]}, + {:p, [], + [ + "For details about the ", + {:code, [], ["PosLen"]}, + " semantics, see ", + {:a, [href: "stdlib:binary", rel: "https://erlang.org/doc/link/seeerl"], [{:code, [], ["binary(3)"]}]}, + "." + ]}, + {:p, [], ["Allowed in guard tests."]} + ] + + actual = HoverHelpers.to_markdown("application/erlang+html", html) + + assert actual == + String.trim(""" + Extracts the part of the binary described by `PosLen`. + + Negative length can be used to extract bytes at the end of a binary, for example: + + ```erlang + 1> Bin = <<1,2,3,4,5,6,7,8,9,10>>. + 2> binary_part(Bin,{byte_size(Bin), -5}). + <<6,7,8,9,10>> + ``` + + Failure: `badarg` if `PosLen` in any way references outside the binary. + + `Start` is zero-based, that is: + + ```erlang + 1> Bin = <<1,2,3>> + 2> binary_part(Bin,{0,2}). + <<1,2>> + ``` + + For details about the `PosLen` semantics, see [`binary(3)`](stdlib:binary). + + Allowed in guard tests. + """) + end + + test "ul and li" do + html = [ + {:ul, [], + [ + {:li, [], + [ + {:p, [], + [ + "Find an arbitrary ", + {:a, + [ + href: "stdlib:digraph#simple_path", + rel: "https://erlang.org/doc/link/seeerl" + ], ["simple path"]}, + " v[1], v[2], ..., v[k] from ", + {:code, [], ["V1"]}, + " to ", + {:code, [], ["V2"]}, + " in ", + {:code, [], ["G"]}, + "." + ]} + ]}, + {:li, [], + [ + {:p, [], + [ + "Remove all edges of ", + {:code, [], ["G"]}, + " ", + {:a, + [ + href: "stdlib:digraph#emanate", + rel: "https://erlang.org/doc/link/seeerl" + ], ["emanating"]}, + " from v[i] and ", + {:a, + [ + href: "stdlib:digraph#incident", + rel: "https://erlang.org/doc/link/seeerl" + ], ["incident"]}, + " to v[i+1] for 1 <= i < k (including multiple edges)." + ]} + ]}, + {:li, [], + [ + {:p, [], + [ + "Repeat until there is no path between ", + {:code, [], ["V1"]}, + " and ", + {:code, [], ["V2"]}, + "." + ]} + ]} + ]} + ] + + actual = HoverHelpers.to_markdown("application/erlang+html", html) + + assert String.trim(actual) == + String.trim(""" + * Find an arbitrary [simple path](stdlib:digraph#simple_path) v[1], v[2], ..., v[k] from `V1` to `V2` in `G`. + * Remove all edges of `G` [emanating](stdlib:digraph#emanate) from v[i] and [incident](stdlib:digraph#incident) to v[i+1] for 1 <= i < k (including multiple edges). + * Repeat until there is no path between `V1` and `V2`. + """) + end + end +end diff --git a/test/next_ls/hover_test.exs b/test/next_ls/hover_test.exs new file mode 100644 index 00000000..6561d89e --- /dev/null +++ b/test/next_ls/hover_test.exs @@ -0,0 +1,378 @@ +defmodule NextLS.HoverTest do + use ExUnit.Case, async: true + + import GenLSP.Test + import NextLS.Support.Utils + + @moduletag :tmp_dir + @moduletag root_paths: ["my_proj"] + setup %{tmp_dir: tmp_dir} do + File.mkdir_p!(Path.join(tmp_dir, "my_proj/lib")) + File.write!(Path.join(tmp_dir, "my_proj/mix.exs"), mix_exs()) + + cwd = Path.join(tmp_dir, "my_proj") + File.mkdir_p!(Path.join([cwd, "lib", "bar"])) + baz = Path.join(cwd, "lib/bar/baz.ex") + + File.write!(baz, """ + defmodule Bar.Baz do + @moduledoc "Bar.Baz module" + @doc "Bar.Baz.q function" + def q do + "q" + end + end + """) + + fiz = Path.join(cwd, "lib/bar/fiz.ex") + + File.write!(fiz, """ + defmodule Bar.Fiz do + # No doc + def q do + "q" + end + end + """) + + guz = Path.join(cwd, "lib/bar/guz.ex") + + File.write!(guz, """ + defmodule Bar.Guz do + alias Foo.Bar + alias Bar.Baz + @moduledoc "Bar.Guz module" + @doc "Bar.Guz.q function" + def q do + "q" + end + end + """) + + foo = Path.join(cwd, "lib/foo.ex") + + File.write!(foo, """ + defmodule Foo do + @moduledoc "Foo module" + @doc "Foo.bar function" + def bar do + "baz" + end + end + """) + + example = Path.join(cwd, "lib/example.ex") + + File.write!(example, """ + defmodule Example do + @moduledoc "Example doc" + alias Foo, as: Foz + alias Bar.{ + Fiz, + Baz + } + alias Bar.Guz + defstruct [:foo] + def test do + q1 = Atom.to_string(:atom) + q2 = Foz.bar() + q3 = Baz.q() + q4 = Fiz.q() + q5 = Guz.q() + q6 = to_string(:abs) + :timer.sleep(1) + q7 = %Example{foo: "a"} + [q1] ++ [q2] ++ [q3] ++ [q4] ++ [q5] ++ [q6] ++ [q7.foo] + end + end + """) + + [example: example] + end + + setup :with_lsp + + setup context do + assert :ok == notify(context.client, %{method: "initialized", jsonrpc: "2.0", params: %{}}) + assert_request(context.client, "client/registerCapability", fn _params -> nil end) + assert_is_ready(context, "my_proj") + assert_compiled(context, "my_proj") + assert_notification "$/progress", %{"value" => %{"kind" => "end", "message" => "Finished indexing!"}} + end + + test "alias calls", %{client: client, example: example} do + example_uri = uri(example) + + request client, %{ + method: "textDocument/hover", + id: 2, + jsonrpc: "2.0", + params: %{ + position: %{line: 2, character: 18}, + textDocument: %{uri: example_uri} + } + } + + assert_result 2, + %{ + "contents" => %{ + "kind" => "markdown", + "value" => "## Foo\n\nFoo module" + }, + "range" => %{ + "start" => %{"character" => 17, "line" => 2}, + "end" => %{"character" => 19, "line" => 2} + } + }, + 500 + + request client, %{ + method: "textDocument/hover", + id: 4, + jsonrpc: "2.0", + params: %{ + position: %{line: 5, character: 5}, + textDocument: %{uri: example_uri} + } + } + + assert_result 4, + %{ + "contents" => %{ + "kind" => "markdown", + "value" => "## Bar.Baz\n\nBar.Baz module" + }, + "range" => %{ + "start" => %{"character" => 4, "line" => 5}, + "end" => %{"character" => 6, "line" => 5} + } + }, + 500 + end + + test "modules", %{client: client, example: example} do + example_uri = uri(example) + + request client, %{ + method: "textDocument/hover", + id: 2, + jsonrpc: "2.0", + params: %{ + position: %{line: 11, character: 10}, + textDocument: %{uri: example_uri} + } + } + + assert_result 2, + %{ + "contents" => %{ + "kind" => "markdown", + "value" => "## Foo\n\nFoo module" + }, + "range" => %{ + "start" => %{"line" => 11, "character" => 9}, + "end" => %{"line" => 11, "character" => 11} + } + }, + 500 + end + + # TODO: this was fixed recently, will emit the elixir docs + test "inlined function", %{client: client, example: example} do + example_uri = uri(example) + + request client, %{ + method: "textDocument/hover", + id: 7, + jsonrpc: "2.0", + params: %{ + position: %{line: 10, character: 18}, + textDocument: %{uri: example_uri} + } + } + + assert_result 7, + %{ + "contents" => %{ + "kind" => "markdown", + "value" => + "## :erlang.atom_to_binary/1\n\n" <> + _ + }, + "range" => %{ + "start" => %{"character" => 14, "line" => 10}, + "end" => %{"character" => 27, "line" => 10} + } + }, + 500 + end + + test "elixir function", %{client: client, example: example} do + example_uri = uri(example) + + request client, %{ + method: "textDocument/hover", + id: 8, + jsonrpc: "2.0", + params: %{ + position: %{line: 12, character: 13}, + textDocument: %{uri: example_uri} + } + } + + assert_result 8, + %{ + "contents" => %{ + "kind" => "markdown", + "value" => "## Bar.Baz.q/0\n\nBar.Baz.q function" + }, + "range" => %{ + "start" => %{"character" => 13, "line" => 12}, + "end" => %{"character" => 13, "line" => 12} + } + }, + 500 + end + + test "module without docs", %{client: client, example: example} do + example_uri = uri(example) + + request client, %{ + method: "textDocument/hover", + id: 9, + jsonrpc: "2.0", + params: %{ + position: %{line: 13, character: 11}, + textDocument: %{uri: example_uri} + } + } + + assert_result 9, nil, 500 + end + + test "function without docs", %{client: client, example: example} do + example_uri = uri(example) + + request client, %{ + method: "textDocument/hover", + id: 10, + jsonrpc: "2.0", + params: %{ + position: %{line: 13, character: 13}, + textDocument: %{uri: example_uri} + } + } + + assert_result 10, nil, 500 + end + + test "imported function", %{client: client, example: example} do + example_uri = uri(example) + + request client, %{ + method: "textDocument/hover", + id: 11, + jsonrpc: "2.0", + params: %{ + position: %{line: 15, character: 12}, + textDocument: %{uri: example_uri} + } + } + + assert_result 11, + %{ + "contents" => %{ + "kind" => "markdown", + "value" => "## Kernel.to_string/1\n\nConverts the argument to a string" <> _ + }, + "range" => %{ + "start" => %{"character" => 9, "line" => 15}, + "end" => %{"character" => 17, "line" => 15} + } + }, + 500 + end + + test "erlang function", %{client: client, example: example} do + example_uri = uri(example) + + request client, %{ + method: "textDocument/hover", + id: 13, + jsonrpc: "2.0", + params: %{ + position: %{line: 16, character: 13}, + textDocument: %{uri: example_uri} + } + } + + assert_result 13, + %{ + "contents" => %{ + "kind" => "markdown", + "value" => + "## :timer.sleep/1\n\nSuspends the process" <> + _ + }, + "range" => %{ + "start" => %{"character" => 11, "line" => 16}, + "end" => %{"character" => 15, "line" => 16} + } + }, + 500 + end + + test "structs", %{client: client, example: example} do + example_uri = uri(example) + + request client, %{ + method: "textDocument/hover", + id: 14, + jsonrpc: "2.0", + params: %{ + position: %{line: 17, character: 13}, + textDocument: %{uri: example_uri} + } + } + + assert_result 14, + %{ + "contents" => %{ + "kind" => "markdown", + "value" => "## Example\n\nExample doc" + }, + "range" => %{ + "start" => %{"character" => 10, "line" => 17}, + "end" => %{"character" => 16, "line" => 17} + } + }, + 500 + end + + test "imported macro", %{client: client, example: example} do + example_uri = uri(example) + + request client, %{ + method: "textDocument/hover", + id: 15, + jsonrpc: "2.0", + params: %{ + position: %{line: 9, character: 3}, + textDocument: %{uri: example_uri} + } + } + + assert_result 15, + %{ + "contents" => %{ + "kind" => "markdown", + "value" => "## Kernel.def/2\n\nDefines a public function with the given name and body" <> _ + }, + "range" => %{ + "start" => %{"character" => 2, "line" => 9}, + "end" => %{"character" => 4, "line" => 9} + } + }, + 500 + end +end diff --git a/test/next_ls/references_test.exs b/test/next_ls/references_test.exs index 9de7bef9..3543280a 100644 --- a/test/next_ls/references_test.exs +++ b/test/next_ls/references_test.exs @@ -79,7 +79,7 @@ defmodule NextLS.ReferencesTest do "uri" => uri, "range" => %{ "start" => %{"line" => 3, "character" => 10}, - "end" => %{"line" => 3, "character" => 18} + "end" => %{"line" => 3, "character" => 17} } } ] @@ -119,7 +119,7 @@ defmodule NextLS.ReferencesTest do "uri" => ^uri, "range" => %{ "start" => %{"line" => 3, "character" => 4}, - "end" => %{"line" => 3, "character" => 9} + "end" => %{"line" => 3, "character" => 8} } } ] @@ -152,14 +152,14 @@ defmodule NextLS.ReferencesTest do "uri" => uri, "range" => %{ "start" => %{"line" => 11, "character" => 14}, - "end" => %{"line" => 11, "character" => 23} + "end" => %{"line" => 11, "character" => 22} } }, %{ "uri" => uri, "range" => %{ "start" => %{"line" => 15, "character" => 13}, - "end" => %{"line" => 15, "character" => 22} + "end" => %{"line" => 15, "character" => 21} } } ]