Skip to content

Commit 2e7ac6d

Browse files
gofenixzhuzhenfeng.code
andauthored
add quick search to hexdocs.pm (#574)
* 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]>
1 parent 8fb095f commit 2e7ac6d

File tree

3 files changed

+246
-6
lines changed

3 files changed

+246
-6
lines changed

apps/language_server/lib/language_server/providers/hover.ex

Lines changed: 122 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,18 @@ defmodule ElixirLS.LanguageServer.Providers.Hover do
55
Hover provider utilizing Elixir Sense
66
"""
77

8-
def hover(text, line, character) do
8+
@hex_base_url "https://hexdocs.pm"
9+
@builtin_flag [
10+
"elixir",
11+
"eex",
12+
"ex_unit",
13+
"iex",
14+
"logger",
15+
"mix"
16+
]
17+
|> Enum.map(fn x -> "lib/#{x}/lib" end)
18+
19+
def hover(text, line, character, project_dir) do
920
response =
1021
case ElixirSense.docs(text, line + 1, character + 1) do
1122
%{subject: ""} ->
@@ -15,7 +26,7 @@ defmodule ElixirLS.LanguageServer.Providers.Hover do
1526
line_text = Enum.at(SourceFile.lines(text), line)
1627
range = highlight_range(line_text, line, character, subject)
1728

18-
%{"contents" => contents(docs), "range" => range}
29+
%{"contents" => contents(docs, subject, project_dir), "range" => range}
1930
end
2031

2132
{:ok, response}
@@ -44,14 +55,120 @@ defmodule ElixirLS.LanguageServer.Providers.Hover do
4455
end)
4556
end
4657

47-
defp contents(%{docs: "No documentation available\n"}) do
58+
defp contents(%{docs: "No documentation available\n"}, _subject, _project_dir) do
4859
[]
4960
end
5061

51-
defp contents(%{docs: markdown}) do
62+
defp contents(%{docs: markdown}, subject, project_dir) do
5263
%{
5364
kind: "markdown",
54-
value: markdown
65+
value: add_hexdocs_link(markdown, subject, project_dir)
5566
}
5667
end
68+
69+
defp add_hexdocs_link(markdown, subject, project_dir) do
70+
[hd | tail] = markdown |> String.split("\n\n")
71+
72+
link = hexdocs_link(hd, subject, project_dir)
73+
74+
case link do
75+
"" ->
76+
markdown
77+
78+
_ ->
79+
hd <> " [view on hexdocs](#{link})\n\n" <> Enum.join(tail, "")
80+
end
81+
end
82+
83+
defp hexdocs_link(hd, subject, project_dir) do
84+
title = hd |> String.replace(">", "") |> String.trim() |> URI.encode()
85+
86+
cond do
87+
erlang_module?(subject) ->
88+
# erlang moudle is not support now.
89+
""
90+
91+
true ->
92+
dep = subject |> root_module_name() |> dep_name(project_dir) |> URI.encode()
93+
94+
cond do
95+
func?(title) ->
96+
if dep != "" do
97+
"#{@hex_base_url}/#{dep}/#{module_name(subject)}.html##{func_name(subject)}/#{params_cnt(title)}"
98+
else
99+
""
100+
end
101+
102+
true ->
103+
if dep != "" do
104+
"#{@hex_base_url}/#{dep}/#{title}.html"
105+
else
106+
""
107+
end
108+
end
109+
end
110+
end
111+
112+
defp func?(s) do
113+
s =~ ~r/.*\..*\(.*\)/
114+
end
115+
116+
defp module_name(s) do
117+
[_ | tail] = s |> String.split(".") |> Enum.reverse()
118+
tail |> Enum.reverse() |> Enum.join(".") |> URI.encode()
119+
end
120+
121+
defp func_name(s) do
122+
s |> String.split(".") |> Enum.at(-1) |> URI.encode()
123+
end
124+
125+
defp params_cnt(s) do
126+
cond do
127+
s =~ ~r/\(\)/ -> 0
128+
not String.contains?(s, ",") -> 1
129+
true -> s |> String.split(",") |> length()
130+
end
131+
end
132+
133+
defp dep_name(root_mod_name, project_dir) do
134+
s = root_mod_name |> source()
135+
136+
cond do
137+
third_dep?(s, project_dir) -> third_dep_name(s, project_dir)
138+
builtin?(s) -> builtin_dep_name(s)
139+
true -> ""
140+
end
141+
end
142+
143+
defp root_module_name(subject) do
144+
subject |> String.split(".") |> hd()
145+
end
146+
147+
defp source(mod_name) do
148+
dep = ("Elixir." <> mod_name) |> String.to_atom()
149+
dep.__info__(:compile) |> Keyword.get(:source) |> List.to_string()
150+
end
151+
152+
defp third_dep?(source, project_dir) do
153+
prefix = project_dir <> "/deps"
154+
String.starts_with?(source, prefix)
155+
end
156+
157+
defp third_dep_name(source, project_dir) do
158+
prefix = project_dir <> "/deps/"
159+
String.replace_prefix(source, prefix, "") |> String.split("/") |> hd()
160+
end
161+
162+
defp builtin?(source) do
163+
@builtin_flag |> Enum.any?(fn y -> String.contains?(source, y) end)
164+
end
165+
166+
defp builtin_dep_name(source) do
167+
[_, name | _] = String.split(source, "/lib/")
168+
name
169+
end
170+
171+
defp erlang_module?(subject) do
172+
subject |> root_module_name() |> String.starts_with?(":")
173+
end
57174
end

apps/language_server/lib/language_server/server.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -600,7 +600,7 @@ defmodule ElixirLS.LanguageServer.Server do
600600
source_file = get_source_file(state, uri)
601601

602602
fun = fn ->
603-
Hover.hover(source_file.text, line, character)
603+
Hover.hover(source_file.text, line, character, state.project_dir)
604604
end
605605

606606
{:async, fun, state}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
defmodule ElixirLS.LanguageServer.Providers.HoverTest do
2+
use ElixirLS.Utils.MixTest.Case, async: false
3+
import ElixirLS.LanguageServer.Test.PlatformTestHelpers
4+
5+
alias ElixirLS.LanguageServer.Providers.Hover
6+
# mix cmd --app language_server mix test test/providers/hover_test.exs
7+
8+
def fake_dir() do
9+
Path.join(__DIR__, "../../../..") |> Path.expand() |> maybe_convert_path_separators()
10+
end
11+
12+
test "blank hover" do
13+
text = """
14+
defmodule MyModule do
15+
def hello() do
16+
IO.inspect("hello world")
17+
end
18+
end
19+
"""
20+
21+
{line, char} = {2, 1}
22+
23+
assert {:ok, resp} = Hover.hover(text, line, char, fake_dir())
24+
assert nil == resp
25+
end
26+
27+
test "Elixir builtin module hover" do
28+
text = """
29+
defmodule MyModule do
30+
def hello() do
31+
IO.inspect("hello world")
32+
end
33+
end
34+
"""
35+
36+
{line, char} = {2, 5}
37+
38+
assert {:ok, %{"contents" => %{kind: "markdown", value: v}}} =
39+
Hover.hover(text, line, char, fake_dir())
40+
41+
assert String.starts_with?(v, "> IO [view on hexdocs](https://hexdocs.pm/elixir/IO.html)")
42+
end
43+
44+
test "Elixir builtin function hover" do
45+
text = """
46+
defmodule MyModule do
47+
def hello() do
48+
IO.inspect("hello world")
49+
end
50+
end
51+
"""
52+
53+
{line, char} = {2, 10}
54+
55+
assert {:ok, %{"contents" => %{kind: "markdown", value: v}}} =
56+
Hover.hover(text, line, char, fake_dir())
57+
58+
assert String.starts_with?(
59+
v,
60+
"> IO.inspect(item, opts \\\\\\\\ []) [view on hexdocs](https://hexdocs.pm/elixir/IO.html#inspect/2)"
61+
)
62+
end
63+
64+
test "Umbrella projects: Third deps module hover" do
65+
text = """
66+
defmodule MyModule do
67+
def hello() do
68+
StreamData.integer() |> Stream.map(&abs/1) |> Enum.take(3) |> IO.inspect()
69+
end
70+
end
71+
"""
72+
73+
{line, char} = {2, 10}
74+
75+
assert {:ok, %{"contents" => %{kind: "markdown", value: v}}} =
76+
Hover.hover(text, line, char, fake_dir())
77+
78+
assert String.starts_with?(
79+
v,
80+
"> StreamData [view on hexdocs](https://hexdocs.pm/stream_data/StreamData.html)"
81+
)
82+
end
83+
84+
test "Umbrella projects: Third deps function hover" do
85+
text = """
86+
defmodule MyModule do
87+
def hello() do
88+
StreamData.integer() |> Stream.map(&abs/1) |> Enum.take(3) |> IO.inspect()
89+
end
90+
end
91+
"""
92+
93+
{line, char} = {2, 18}
94+
95+
assert {:ok, %{"contents" => %{kind: "markdown", value: v}}} =
96+
Hover.hover(text, line, char, fake_dir())
97+
98+
assert String.starts_with?(
99+
v,
100+
"> StreamData.integer() [view on hexdocs](https://hexdocs.pm/stream_data/StreamData.html#integer/0)"
101+
)
102+
end
103+
104+
test "Erlang module hover is not support now" do
105+
text = """
106+
defmodule MyModule do
107+
def hello() do
108+
:timer.sleep(1000)
109+
end
110+
end
111+
"""
112+
113+
{line, char} = {2, 10}
114+
115+
assert {:ok, %{"contents" => %{kind: "markdown", value: v}}} =
116+
Hover.hover(text, line, char, fake_dir())
117+
118+
assert not String.contains?(
119+
v,
120+
"[view on hexdocs]"
121+
)
122+
end
123+
end

0 commit comments

Comments
 (0)