Skip to content

Commit 6a2ad26

Browse files
committed
Improve hover results
- Use jedi Script.help instead of infer - Ensures that docstrings of the definition are used if possible instead of docstrings of the type - Show all possible signatures or types of the element hovered instead of just the first one that matches the name
1 parent 05698fa commit 6a2ad26

File tree

2 files changed

+180
-44
lines changed

2 files changed

+180
-44
lines changed

pylsp/plugins/hover.py

+64-26
Original file line numberDiff line numberDiff line change
@@ -8,43 +8,81 @@
88
log = logging.getLogger(__name__)
99

1010

11+
def _find_docstring(definitions):
12+
if len(definitions) != 1:
13+
# Either no definitions or multiple definitions
14+
# If we have multiple definitions the element can be multiple things and we
15+
# do not know which one
16+
17+
# TODO(Review)
18+
# We could also concatenate all docstrings we find in the definitions
19+
# I am agains this because
20+
# - If just one definition has a docstring, it gives a false impression of the hover element
21+
# - If multiple definitions have a docstring, the user will probably not relize
22+
# that he can scroll to see the other options
23+
return ""
24+
25+
# The single true definition
26+
definition = definitions[0]
27+
docstring = definition.docstring(
28+
raw=True
29+
) # raw docstring returns only doc, without signature
30+
if docstring != "":
31+
return docstring
32+
33+
# If the definition has no docstring, try to infer the type
34+
types = definition.infer()
35+
36+
if len(types) != 1:
37+
# If we have multiple types the element can be multiple things and we
38+
# do not know which one
39+
return ""
40+
41+
# Use the docstring of the single true type (possibly empty)
42+
return types[0].docstring(raw=True)
43+
44+
45+
def _find_signatures(definitions, word):
46+
# Get the signatures of all definitions
47+
signatures = [
48+
signature.to_string()
49+
for definition in definitions
50+
for signature in definition.get_signatures()
51+
if signature.type not in ["module"]
52+
]
53+
54+
if len(signatures) != 0:
55+
return signatures
56+
57+
# If we did not find a signature, infer the possible types of all definitions
58+
types = [
59+
t.name
60+
for d in sorted(definitions, key=lambda d: d.line)
61+
for t in sorted(d.infer(), key=lambda t: t.line)
62+
]
63+
if len(types) == 1:
64+
return [types[0]]
65+
elif len(types) > 1:
66+
return [f"Union[{', '.join(types)}]"]
67+
68+
1169
@hookimpl
1270
def pylsp_hover(config, document, position):
1371
code_position = _utils.position_to_jedi_linecolumn(document, position)
14-
definitions = document.jedi_script(use_document_path=True).infer(**code_position)
15-
word = document.word_at_position(position)
1672

17-
# Find first exact matching definition
18-
definition = next((x for x in definitions if x.name == word), None)
19-
20-
# Ensure a definition is used if only one is available
21-
# even if the word doesn't match. An example of this case is 'np'
22-
# where 'numpy' doesn't match with 'np'. Same for NumPy ufuncs
23-
if len(definitions) == 1:
24-
definition = definitions[0]
25-
26-
if not definition:
27-
return {"contents": ""}
73+
# TODO(Review)
74+
# We could also use Script.help here. It would not resolve keywords
75+
definitions = document.jedi_script(use_document_path=True).help(**code_position)
76+
word = document.word_at_position(position)
2877

2978
hover_capabilities = config.capabilities.get("textDocument", {}).get("hover", {})
3079
supported_markup_kinds = hover_capabilities.get("contentFormat", ["markdown"])
3180
preferred_markup_kind = _utils.choose_markup_kind(supported_markup_kinds)
3281

33-
# Find first exact matching signature
34-
signature = next(
35-
(
36-
x.to_string()
37-
for x in definition.get_signatures()
38-
if (x.name == word and x.type not in ["module"])
39-
),
40-
"",
41-
)
42-
4382
return {
4483
"contents": _utils.format_docstring(
45-
# raw docstring returns only doc, without signature
46-
definition.docstring(raw=True),
84+
_find_docstring(definitions),
4785
preferred_markup_kind,
48-
signatures=[signature] if signature else None,
86+
signatures=_find_signatures(definitions, word),
4987
)
5088
}

test/plugins/test_hover.py

+116-18
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,44 @@
99

1010
DOC_URI = uris.from_fs_path(__file__)
1111
DOC = """
12+
from random import randint
13+
from typing import overload
1214
13-
def main():
14-
\"\"\"hello world\"\"\"
15+
class A:
16+
\"\"\"Docstring for class A\"\"\"
17+
18+
b = 42
19+
\"\"\"Docstring for the class property A.b\"\"\"
20+
21+
def foo(self):
22+
\"\"\"Docstring for A.foo\"\"\"
23+
pass
24+
25+
if randint(0, 1) == 0:
26+
int_or_string_value = 10
27+
else:
28+
int_or_string_value = "10"
29+
30+
@overload
31+
def overload_function(s: int) -> int:
32+
...
33+
34+
@overload
35+
def overload_function(s: str) -> str:
36+
...
37+
38+
def overload_function(s):
39+
\"\"\"Docstring of overload function\"\"\"
1540
pass
41+
42+
int_value = 10
43+
string_value = "foo"
44+
instance_of_a = A()
45+
copy_of_class_a = A
46+
copy_of_property_b = A.b
47+
int_or_string_value
48+
overload_function
49+
1650
"""
1751

1852
NUMPY_DOC = """
@@ -23,6 +57,83 @@ def main():
2357
"""
2458

2559

60+
def _hover_result_in_doc(workspace, position):
61+
doc = Document(DOC_URI, workspace, DOC)
62+
return pylsp_hover(
63+
doc._config, doc, {"line": position[0], "character": position[1]}
64+
)["contents"]["value"]
65+
66+
67+
def test_hover_over_nothing(workspace):
68+
# Over blank line
69+
assert "" == _hover_result_in_doc(workspace, (3, 0))
70+
71+
72+
def test_hover_on_keyword(workspace):
73+
# Over "class" in "class A:"
74+
res = _hover_result_in_doc(workspace, (4, 1))
75+
assert "Class definitions" in res
76+
77+
78+
def test_hover_on_variables(workspace):
79+
# Over "int_value" in "int_value = 10"
80+
res = _hover_result_in_doc(workspace, (31, 2))
81+
assert "int" in res # type
82+
83+
# Over "string_value" in "string_value = "foo""
84+
res = _hover_result_in_doc(workspace, (32, 2))
85+
assert "string" in res # type
86+
87+
88+
def test_hover_on_class(workspace):
89+
# Over "A" in "class A:"
90+
res = _hover_result_in_doc(workspace, (4, 7))
91+
assert "A()" in res # signature
92+
assert "Docstring for class A" in res # docstring
93+
94+
# Over "A" in "instance_of_a = A()"
95+
res = _hover_result_in_doc(workspace, (33, 17))
96+
assert "A()" in res # signature
97+
assert "Docstring for class A" in res # docstring
98+
99+
# Over "copy_of_class_a" in "copy_of_class_a = A" - needs infer
100+
res = _hover_result_in_doc(workspace, (34, 4))
101+
assert "A()" in res # signature
102+
assert "Docstring for class A" in res # docstring
103+
104+
105+
def test_hover_on_property(workspace):
106+
# Over "b" in "b = 42"
107+
res = _hover_result_in_doc(workspace, (7, 5))
108+
assert "int" in res # type
109+
assert "Docstring for the class property A.b" in res # docstring
110+
111+
# Over "b" in "A.b"
112+
res = _hover_result_in_doc(workspace, (35, 24))
113+
assert "int" in res # type
114+
assert "Docstring for the class property A.b" in res # docstring
115+
116+
117+
def test_hover_on_method(workspace):
118+
# Over "foo" in "def foo(self):"
119+
res = _hover_result_in_doc(workspace, (10, 10))
120+
assert "foo(self)" in res # signature
121+
assert "Docstring for A.foo" in res # docstring
122+
123+
124+
def test_hover_multiple_definitions(workspace):
125+
# Over "int_or_string_value"
126+
res = _hover_result_in_doc(workspace, (36, 5))
127+
assert "```python\nUnion[int, str]\n```" == res.strip() # only type
128+
129+
# Over "overload_function"
130+
res = _hover_result_in_doc(workspace, (37, 5))
131+
assert (
132+
"overload_function(s: int) -> int\noverload_function(s: str) -> str" in res
133+
) # signature
134+
assert "Docstring of overload function" in res # docstring
135+
136+
26137
def test_numpy_hover(workspace):
27138
# Over the blank line
28139
no_hov_position = {"line": 1, "character": 0}
@@ -38,7 +149,9 @@ def test_numpy_hover(workspace):
38149
doc = Document(DOC_URI, workspace, NUMPY_DOC)
39150

40151
contents = ""
41-
assert contents in pylsp_hover(doc._config, doc, no_hov_position)["contents"]
152+
assert (
153+
contents in pylsp_hover(doc._config, doc, no_hov_position)["contents"]["value"]
154+
)
42155

43156
contents = "NumPy\n=====\n\nProvides\n"
44157
assert (
@@ -72,21 +185,6 @@ def test_numpy_hover(workspace):
72185
)
73186

74187

75-
def test_hover(workspace):
76-
# Over 'main' in def main():
77-
hov_position = {"line": 2, "character": 6}
78-
# Over the blank second line
79-
no_hov_position = {"line": 1, "character": 0}
80-
81-
doc = Document(DOC_URI, workspace, DOC)
82-
83-
contents = {"kind": "markdown", "value": "```python\nmain()\n```\n\n\nhello world"}
84-
85-
assert {"contents": contents} == pylsp_hover(doc._config, doc, hov_position)
86-
87-
assert {"contents": ""} == pylsp_hover(doc._config, doc, no_hov_position)
88-
89-
90188
def test_document_path_hover(workspace_other_root_path, tmpdir):
91189
# Create a dummy module out of the workspace's root_path and try to get
92190
# a definition on it in another file placed next to it.

0 commit comments

Comments
 (0)