Skip to content

Commit

Permalink
add quick search to hexdocs.pm (#574)
Browse files Browse the repository at this point in the history
* add quick search to hexdocs.pm

Change-Id: Ifd888136b0c7cd9936bf02612bd87fdcce639ba7

* add hexdoc link for elixir std modlue and func

Change-Id: I2f858ccab0a13fac426666263d59f4cf215db8a4

* parse deps at compile to gen hexdocs link

Change-Id: Ief6e0fd0c13b2e42ddaf1817dfbf20b600971011

* fix ci

Change-Id: I25228974078c7f09c6afc748c22144556b5cdc63

* fix ci

Change-Id: I7e6debee937d2e4de5a3b990e4086b59e78a3aba

* pass project_dir from server

Change-Id: Iff48c16cf6a5d518004f7f211235e0be085985f2

* add some test for hover

Change-Id: Id40baed74aeecf62f086f158185ed03345ef5ee0

* filter erlang module

Change-Id: I77d984c72513b75d84958bdc63db093b7028142f

Co-authored-by: zhuzhenfeng.code <[email protected]>
  • Loading branch information
gofenix and zhuzhenfeng.code authored Jan 23, 2022
1 parent 8fb095f commit 2e7ac6d
Show file tree
Hide file tree
Showing 3 changed files with 246 additions and 6 deletions.
127 changes: 122 additions & 5 deletions apps/language_server/lib/language_server/providers/hover.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,18 @@ defmodule ElixirLS.LanguageServer.Providers.Hover do
Hover provider utilizing Elixir Sense
"""

def hover(text, line, character) do
@hex_base_url "https://hexdocs.pm"
@builtin_flag [
"elixir",
"eex",
"ex_unit",
"iex",
"logger",
"mix"
]
|> Enum.map(fn x -> "lib/#{x}/lib" end)

def hover(text, line, character, project_dir) do
response =
case ElixirSense.docs(text, line + 1, character + 1) do
%{subject: ""} ->
Expand All @@ -15,7 +26,7 @@ defmodule ElixirLS.LanguageServer.Providers.Hover do
line_text = Enum.at(SourceFile.lines(text), line)
range = highlight_range(line_text, line, character, subject)

%{"contents" => contents(docs), "range" => range}
%{"contents" => contents(docs, subject, project_dir), "range" => range}
end

{:ok, response}
Expand Down Expand Up @@ -44,14 +55,120 @@ defmodule ElixirLS.LanguageServer.Providers.Hover do
end)
end

defp contents(%{docs: "No documentation available\n"}) do
defp contents(%{docs: "No documentation available\n"}, _subject, _project_dir) do
[]
end

defp contents(%{docs: markdown}) do
defp contents(%{docs: markdown}, subject, project_dir) do
%{
kind: "markdown",
value: markdown
value: add_hexdocs_link(markdown, subject, project_dir)
}
end

defp add_hexdocs_link(markdown, subject, project_dir) do
[hd | tail] = markdown |> String.split("\n\n")

link = hexdocs_link(hd, subject, project_dir)

case link do
"" ->
markdown

_ ->
hd <> " [view on hexdocs](#{link})\n\n" <> Enum.join(tail, "")
end
end

defp hexdocs_link(hd, subject, project_dir) do
title = hd |> String.replace(">", "") |> String.trim() |> URI.encode()

cond do
erlang_module?(subject) ->
# erlang moudle is not support now.
""

true ->
dep = subject |> root_module_name() |> dep_name(project_dir) |> URI.encode()

cond do
func?(title) ->
if dep != "" do
"#{@hex_base_url}/#{dep}/#{module_name(subject)}.html##{func_name(subject)}/#{params_cnt(title)}"
else
""
end

true ->
if dep != "" do
"#{@hex_base_url}/#{dep}/#{title}.html"
else
""
end
end
end
end

defp func?(s) do
s =~ ~r/.*\..*\(.*\)/
end

defp module_name(s) do
[_ | tail] = s |> String.split(".") |> Enum.reverse()
tail |> Enum.reverse() |> Enum.join(".") |> URI.encode()
end

defp func_name(s) do
s |> String.split(".") |> Enum.at(-1) |> URI.encode()
end

defp params_cnt(s) do
cond do
s =~ ~r/\(\)/ -> 0
not String.contains?(s, ",") -> 1
true -> s |> String.split(",") |> length()
end
end

defp dep_name(root_mod_name, project_dir) do
s = root_mod_name |> source()

cond do
third_dep?(s, project_dir) -> third_dep_name(s, project_dir)
builtin?(s) -> builtin_dep_name(s)
true -> ""
end
end

defp root_module_name(subject) do
subject |> String.split(".") |> hd()
end

defp source(mod_name) do
dep = ("Elixir." <> mod_name) |> String.to_atom()
dep.__info__(:compile) |> Keyword.get(:source) |> List.to_string()
end

defp third_dep?(source, project_dir) do
prefix = project_dir <> "/deps"
String.starts_with?(source, prefix)
end

defp third_dep_name(source, project_dir) do
prefix = project_dir <> "/deps/"
String.replace_prefix(source, prefix, "") |> String.split("/") |> hd()
end

defp builtin?(source) do
@builtin_flag |> Enum.any?(fn y -> String.contains?(source, y) end)
end

defp builtin_dep_name(source) do
[_, name | _] = String.split(source, "/lib/")
name
end

defp erlang_module?(subject) do
subject |> root_module_name() |> String.starts_with?(":")
end
end
2 changes: 1 addition & 1 deletion apps/language_server/lib/language_server/server.ex
Original file line number Diff line number Diff line change
Expand Up @@ -600,7 +600,7 @@ defmodule ElixirLS.LanguageServer.Server do
source_file = get_source_file(state, uri)

fun = fn ->
Hover.hover(source_file.text, line, character)
Hover.hover(source_file.text, line, character, state.project_dir)
end

{:async, fun, state}
Expand Down
123 changes: 123 additions & 0 deletions apps/language_server/test/providers/hover_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
defmodule ElixirLS.LanguageServer.Providers.HoverTest do
use ElixirLS.Utils.MixTest.Case, async: false
import ElixirLS.LanguageServer.Test.PlatformTestHelpers

alias ElixirLS.LanguageServer.Providers.Hover
# mix cmd --app language_server mix test test/providers/hover_test.exs

def fake_dir() do
Path.join(__DIR__, "../../../..") |> Path.expand() |> maybe_convert_path_separators()
end

test "blank hover" do
text = """
defmodule MyModule do
def hello() do
IO.inspect("hello world")
end
end
"""

{line, char} = {2, 1}

assert {:ok, resp} = Hover.hover(text, line, char, fake_dir())
assert nil == resp
end

test "Elixir builtin module hover" do
text = """
defmodule MyModule do
def hello() do
IO.inspect("hello world")
end
end
"""

{line, char} = {2, 5}

assert {:ok, %{"contents" => %{kind: "markdown", value: v}}} =
Hover.hover(text, line, char, fake_dir())

assert String.starts_with?(v, "> IO [view on hexdocs](https://hexdocs.pm/elixir/IO.html)")
end

test "Elixir builtin function hover" do
text = """
defmodule MyModule do
def hello() do
IO.inspect("hello world")
end
end
"""

{line, char} = {2, 10}

assert {:ok, %{"contents" => %{kind: "markdown", value: v}}} =
Hover.hover(text, line, char, fake_dir())

assert String.starts_with?(
v,
"> IO.inspect(item, opts \\\\\\\\ []) [view on hexdocs](https://hexdocs.pm/elixir/IO.html#inspect/2)"
)
end

test "Umbrella projects: Third deps module hover" do
text = """
defmodule MyModule do
def hello() do
StreamData.integer() |> Stream.map(&abs/1) |> Enum.take(3) |> IO.inspect()
end
end
"""

{line, char} = {2, 10}

assert {:ok, %{"contents" => %{kind: "markdown", value: v}}} =
Hover.hover(text, line, char, fake_dir())

assert String.starts_with?(
v,
"> StreamData [view on hexdocs](https://hexdocs.pm/stream_data/StreamData.html)"
)
end

test "Umbrella projects: Third deps function hover" do
text = """
defmodule MyModule do
def hello() do
StreamData.integer() |> Stream.map(&abs/1) |> Enum.take(3) |> IO.inspect()
end
end
"""

{line, char} = {2, 18}

assert {:ok, %{"contents" => %{kind: "markdown", value: v}}} =
Hover.hover(text, line, char, fake_dir())

assert String.starts_with?(
v,
"> StreamData.integer() [view on hexdocs](https://hexdocs.pm/stream_data/StreamData.html#integer/0)"
)
end

test "Erlang module hover is not support now" do
text = """
defmodule MyModule do
def hello() do
:timer.sleep(1000)
end
end
"""

{line, char} = {2, 10}

assert {:ok, %{"contents" => %{kind: "markdown", value: v}}} =
Hover.hover(text, line, char, fake_dir())

assert not String.contains?(
v,
"[view on hexdocs]"
)
end
end

0 comments on commit 2e7ac6d

Please sign in to comment.