Skip to content

Commit 2d5ff5d

Browse files
committed
feat: Change hover to use MarkupContent
MarkedString[] has been deprecated so we switch to full Markdown syntax for hover messages Fixes #45
1 parent 283ec3f commit 2d5ff5d

File tree

6 files changed

+261
-196
lines changed

6 files changed

+261
-196
lines changed

fortls/intrinsics.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def get_signature(self):
7171
return call_sig, self.doc_str, arg_sigs
7272

7373
def get_hover(self, long=False):
74-
return self.doc_str, False
74+
return self.doc_str, None, False
7575

7676
def is_callable(self):
7777
if self.type == 2:

fortls/langserver.py

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -490,9 +490,10 @@ def build_comp(
490490
comp_obj["detail"] = candidate.get_desc()
491491
if call_sig is not None:
492492
comp_obj["detail"] += " " + call_sig
493-
doc_str, _ = candidate.get_hover()
494-
if doc_str is not None:
495-
comp_obj["documentation"] = doc_str
493+
# TODO: doc_str should probably be appended, see LSP standard
494+
hover_msg, doc_str, _ = candidate.get_hover()
495+
if hover_msg is not None:
496+
comp_obj["documentation"] = hover_msg
496497
return comp_obj
497498

498499
# Get parameters from request
@@ -1035,11 +1036,13 @@ def serve_definition(self, request: dict):
10351036
return None
10361037

10371038
def serve_hover(self, request: dict):
1038-
def create_hover(string: str, highlight: bool):
1039-
if highlight:
1040-
return {"language": self.hover_language, "value": string}
1041-
else:
1042-
return string
1039+
def create_hover(string: str, docs: str | None, fortran: bool):
1040+
msg = string
1041+
if fortran:
1042+
msg = f"```{self.hover_language}\n{string}\n```"
1043+
if docs: # if docs is not None or ""
1044+
msg += f"\n-----\n{docs}"
1045+
return msg
10431046

10441047
def create_signature_hover():
10451048
sig_request = request.copy()
@@ -1057,7 +1060,8 @@ def create_signature_hover():
10571060
f"{arg_doc[:doc_split]} :: "
10581061
f"{arg_info['label']}{arg_doc[doc_split:]}"
10591062
)
1060-
return create_hover(arg_string, True)
1063+
# TODO: check if correct. I think it's not
1064+
return create_hover(arg_string, None, True)
10611065
except:
10621066
pass
10631067

@@ -1066,7 +1070,7 @@ def create_signature_hover():
10661070
uri: str = params["textDocument"]["uri"]
10671071
def_line: int = params["position"]["line"]
10681072
def_char: int = params["position"]["character"]
1069-
path = path_from_uri(uri)
1073+
path: str = path_from_uri(uri)
10701074
file_obj = self.workspace.get(path)
10711075
if file_obj is None:
10721076
return None
@@ -1075,41 +1079,40 @@ def create_signature_hover():
10751079
if var_obj is None:
10761080
return None
10771081
# Construct hover information
1078-
var_type = var_obj.get_type()
1082+
var_type: int = var_obj.get_type()
10791083
hover_array = []
10801084
if var_type in (SUBROUTINE_TYPE_ID, FUNCTION_TYPE_ID):
1081-
hover_str, highlight = var_obj.get_hover(long=True)
1082-
hover_array.append(create_hover(hover_str, highlight))
1085+
hover_str, docs, highlight = var_obj.get_hover(long=True)
1086+
hover_array.append(create_hover(hover_str, docs, highlight))
10831087
elif var_type == INTERFACE_TYPE_ID:
10841088
for member in var_obj.mems:
1085-
hover_str, highlight = member.get_hover(long=True)
1089+
hover_str, docs, highlight = member.get_hover(long=True)
10861090
if hover_str is not None:
1087-
hover_array.append(create_hover(hover_str, highlight))
1091+
hover_array.append(create_hover(hover_str, docs, highlight))
10881092
elif var_type == VAR_TYPE_ID:
10891093
# Unless we have a Fortran literal include the desc in the hover msg
10901094
# See get_definition for an explanation about this default name
10911095
if not var_obj.desc.startswith(FORTRAN_LITERAL):
1092-
hover_str, highlight = var_obj.get_hover()
1093-
hover_array.append(create_hover(hover_str, highlight))
1096+
hover_str, docs, highlight = var_obj.get_hover()
1097+
hover_array.append(create_hover(hover_str, docs, highlight))
10941098
# Hover for Literal variables
10951099
elif var_obj.desc.endswith("REAL"):
1096-
hover_array.append(create_hover("REAL", True))
1100+
hover_array.append(create_hover("REAL", None, True))
10971101
elif var_obj.desc.endswith("INTEGER"):
1098-
hover_array.append(create_hover("INTEGER", True))
1102+
hover_array.append(create_hover("INTEGER", None, True))
10991103
elif var_obj.desc.endswith("LOGICAL"):
1100-
hover_array.append(create_hover("LOGICAL", True))
1104+
hover_array.append(create_hover("LOGICAL", None, True))
11011105
elif var_obj.desc.endswith("STRING"):
11021106
hover_str = f"CHARACTER(LEN={len(var_obj.name)-2})"
1103-
hover_array.append(create_hover(hover_str, True))
1107+
hover_array.append(create_hover(hover_str, None, True))
11041108

11051109
# Include the signature if one is present e.g. if in an argument list
11061110
if self.hover_signature:
1107-
hover_str = create_signature_hover()
1111+
hover_str: str | None = create_signature_hover()
11081112
if hover_str is not None:
11091113
hover_array.append(hover_str)
1110-
#
11111114
if len(hover_array) > 0:
1112-
return {"contents": hover_array}
1115+
return {"contents": {"kind": "markdown", "value": "\n".join(hover_array)}}
11131116
return None
11141117

11151118
def serve_implementation(self, request: dict):

fortls/objects.py

Lines changed: 83 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -409,8 +409,8 @@ def get_placeholders(arg_list: list[str]):
409409
def get_documentation(self):
410410
return self.doc_str
411411

412-
def get_hover(self, long=False, include_doc=True, drop_arg=-1):
413-
return None, False
412+
def get_hover(self, long=False, drop_arg=-1) -> tuple[str | None, str | None, bool]:
413+
return None, None, False
414414

415415
def get_signature(self, drop_arg=-1):
416416
return None, None, None
@@ -920,30 +920,52 @@ def get_snippet(self, name_replace=None, drop_arg=-1):
920920
def get_desc(self):
921921
return "SUBROUTINE"
922922

923-
def get_hover(self, long=False, include_doc=True, drop_arg=-1):
923+
def get_hover(self, long=False, drop_arg=-1):
924924
sub_sig, _ = self.get_snippet(drop_arg=drop_arg)
925925
keyword_list = get_keywords(self.keywords)
926926
keyword_list.append(f"{self.get_desc()} ")
927927
hover_array = [" ".join(keyword_list) + sub_sig]
928-
hover_array = self.get_docs_full(hover_array, long, include_doc, drop_arg)
929-
return "\n ".join(hover_array), long
928+
hover_array, docs = self.get_docs_full(hover_array, long, drop_arg)
929+
return "\n ".join(hover_array), " \n".join(docs), long
930930

931931
def get_docs_full(
932-
self, hover_array: list[str], long=False, include_doc=True, drop_arg=-1
933-
):
932+
self, hover_array: list[str], long=False, drop_arg=-1
933+
) -> tuple[list[str], list[str]]:
934+
"""Construct the full documentation with the code signature and the
935+
documentation string + the documentation of any arguments.
936+
937+
Parameters
938+
----------
939+
hover_array : list[str]
940+
The list of strings to append the documentation to.
941+
long : bool, optional
942+
Whether or not to fetch the docs of the arguments, by default False
943+
drop_arg : int, optional
944+
Whether or not to drop certain arguments from the results, by default -1
945+
946+
Returns
947+
-------
948+
tuple[list[str], list[str]]
949+
Tuple containing the Fortran signature that should be in code blocks
950+
and the documentation string that should be in normal Markdown.
951+
"""
952+
doc_strs: list[str] = []
934953
doc_str = self.get_documentation()
935-
if include_doc and doc_str is not None:
936-
hover_array[0] += "\n" + doc_str
954+
if doc_str is not None:
955+
doc_strs.append(doc_str)
937956
if long:
957+
has_args = True
938958
for i, arg_obj in enumerate(self.arg_objs):
939959
if arg_obj is None or i == drop_arg:
940960
continue
941-
arg_doc, _ = arg_obj.get_hover(include_doc=False)
942-
hover_array.append(f"{arg_doc} :: {arg_obj.name}")
943-
doc_str = arg_obj.get_documentation()
944-
if include_doc and (doc_str is not None):
945-
hover_array += doc_str.splitlines()
946-
return hover_array
961+
arg, doc_str, _ = arg_obj.get_hover()
962+
hover_array.append(arg)
963+
if doc_str: # If doc_str is not None or ""
964+
if has_args:
965+
doc_strs.append("\n**Parameters:** ")
966+
has_args = False
967+
doc_strs.append(f"`{arg_obj.name}` {doc_str}")
968+
return hover_array, doc_strs
947969

948970
def get_signature(self, drop_arg=-1):
949971
arg_sigs = []
@@ -964,14 +986,15 @@ def get_signature(self, drop_arg=-1):
964986
call_sig, _ = self.get_snippet()
965987
return call_sig, self.get_documentation(), arg_sigs
966988

989+
# TODO: fix this
967990
def get_interface_array(
968991
self, keywords: list[str], signature: str, change_arg=-1, change_strings=None
969992
):
970993
interface_array = [" ".join(keywords) + signature]
971994
for i, arg_obj in enumerate(self.arg_objs):
972995
if arg_obj is None:
973996
return None
974-
arg_doc, _ = arg_obj.get_hover(include_doc=False)
997+
arg_doc, docs, _ = arg_obj.get_hover()
975998
if i == change_arg:
976999
i0 = arg_doc.lower().find(change_strings[0].lower())
9771000
if i0 >= 0:
@@ -1087,8 +1110,8 @@ def is_callable(self):
10871110
return False
10881111

10891112
def get_hover(
1090-
self, long: bool = False, include_doc: bool = True, drop_arg: int = -1
1091-
) -> tuple[str, bool]:
1113+
self, long: bool = False, drop_arg: int = -1
1114+
) -> tuple[str, str, bool]:
10921115
"""Construct the hover message for a FUNCTION.
10931116
Two forms are produced here the `long` i.e. the normal for hover requests
10941117
@@ -1107,8 +1130,6 @@ def get_hover(
11071130
----------
11081131
long : bool, optional
11091132
toggle between long and short hover results, by default False
1110-
include_doc : bool, optional
1111-
if to include any documentation, by default True
11121133
drop_arg : int, optional
11131134
Ignore argument at position `drop_arg` in the argument list, by default -1
11141135
@@ -1124,17 +1145,21 @@ def get_hover(
11241145
keyword_list.append("FUNCTION")
11251146

11261147
hover_array = [f"{' '.join(keyword_list)} {fun_sig}"]
1127-
hover_array = self.get_docs_full(hover_array, long, include_doc, drop_arg)
1148+
hover_array, docs = self.get_docs_full(hover_array, long, drop_arg)
11281149
# Only append the return value if using long form
11291150
if self.result_obj and long:
1130-
arg_doc, _ = self.result_obj.get_hover(include_doc=False)
1131-
hover_array.append(f"{arg_doc} :: {self.result_obj.name}")
1151+
# Parse the documentation from the result variable
1152+
arg_doc, doc_str, _ = self.result_obj.get_hover()
1153+
if doc_str is not None:
1154+
docs.append(f"\n**Return:** \n`{self.result_obj.name}`{doc_str}")
1155+
hover_array.append(arg_doc)
11321156
# intrinsic functions, where the return type is missing but can be inferred
11331157
elif self.result_type and long:
11341158
# prepend type to function signature
11351159
hover_array[0] = f"{self.result_type} {hover_array[0]}"
1136-
return "\n ".join(hover_array), long
1160+
return "\n ".join(hover_array), " \n".join(docs), long
11371161

1162+
# TODO: fix this
11381163
def get_interface(self, name_replace=None, change_arg=-1, change_strings=None):
11391164
fun_sig, _ = self.get_snippet(name_replace=name_replace)
11401165
fun_sig += f" RESULT({self.result_name})"
@@ -1149,7 +1174,7 @@ def get_interface(self, name_replace=None, change_arg=-1, change_strings=None):
11491174
keyword_list, fun_sig, change_arg, change_strings
11501175
)
11511176
if self.result_obj is not None:
1152-
arg_doc, _ = self.result_obj.get_hover(include_doc=False)
1177+
arg_doc, docs, _ = self.result_obj.get_hover()
11531178
interface_array.append(f"{arg_doc} :: {self.result_obj.name}")
11541179
name = self.name
11551180
if name_replace is not None:
@@ -1656,18 +1681,17 @@ def get_snippet(self, name_replace=None, drop_arg=-1):
16561681
# Normal variable
16571682
return None, None
16581683

1659-
def get_hover(self, long=False, include_doc=True, drop_arg=-1):
1684+
def get_hover(self, long=False, drop_arg=-1) -> tuple[str, str, bool]:
16601685
doc_str = self.get_documentation()
16611686
# In associated blocks we need to fetch the desc and keywords of the
16621687
# linked object
16631688
hover_str = ", ".join([self.get_desc()] + self.get_keywords())
1664-
# TODO: at this stage we can mae this lowercase
1665-
# Add parameter value in the output
1689+
# If this is not a preprocessor variable, we can append the variable name
1690+
if not hover_str.startswith("#"):
1691+
hover_str += f" :: {self.name}"
16661692
if self.is_parameter() and self.param_val:
1667-
hover_str += f" :: {self.name} = {self.param_val}"
1668-
if include_doc and (doc_str is not None):
1669-
hover_str += "\n {}".format("\n ".join(doc_str.splitlines()))
1670-
return hover_str, True
1693+
hover_str += f" = {self.param_val}"
1694+
return hover_str, doc_str, True
16711695

16721696
def get_keywords(self):
16731697
# TODO: if local keywords are set they should take precedence over link_obj
@@ -1803,45 +1827,34 @@ def get_documentation(self):
18031827
return self.link_obj.get_documentation()
18041828
return self.doc_str
18051829

1806-
def get_hover(self, long=False, include_doc=True, drop_arg=-1):
1807-
doc_str = self.get_documentation()
1808-
if long:
1809-
if self.link_obj is None:
1810-
sub_sig, _ = self.get_snippet()
1811-
hover_str = f"{self.get_desc()} {sub_sig}"
1812-
if include_doc and (doc_str is not None):
1813-
hover_str += f"\n{doc_str}"
1814-
else:
1815-
link_hover, _ = self.link_obj.get_hover(
1816-
long=True, include_doc=include_doc, drop_arg=self.drop_arg
1817-
)
1818-
hover_split = link_hover.splitlines()
1819-
call_sig = hover_split[0]
1820-
paren_start = call_sig.rfind("(")
1821-
link_name_len = len(self.link_obj.name)
1822-
call_sig = (
1823-
call_sig[: paren_start - link_name_len]
1824-
+ self.name
1825-
+ call_sig[paren_start:]
1826-
)
1827-
hover_split = hover_split[1:]
1828-
if include_doc and (self.doc_str is not None):
1829-
# Replace linked docs with current object's docs
1830-
if (len(hover_split) > 0) and (hover_split[0].count("!!") > 0):
1831-
for (i, hover_line) in enumerate(hover_split):
1832-
if hover_line.count("!!") == 0:
1833-
hover_split = hover_split[i:]
1834-
break
1835-
else: # All lines are docs
1836-
hover_split = []
1837-
hover_split = [self.doc_str] + hover_split
1838-
hover_str = "\n".join([call_sig] + hover_split)
1839-
return hover_str, True
1840-
else:
1830+
def get_hover(self, long=False, drop_arg=-1) -> tuple[str, str, bool]:
1831+
docs = self.get_documentation()
1832+
if not long:
18411833
hover_str = ", ".join([self.desc] + get_keywords(self.keywords))
1842-
if include_doc and (doc_str is not None):
1843-
hover_str += f"\n{doc_str}"
1844-
return hover_str, True
1834+
return hover_str, docs, True
1835+
# Long hover message
1836+
if self.link_obj is None:
1837+
sub_sig, _ = self.get_snippet()
1838+
hover_str = f"{self.get_desc()} {sub_sig}"
1839+
else:
1840+
link_msg, link_docs, _ = self.link_obj.get_hover(
1841+
long=True, drop_arg=self.drop_arg
1842+
)
1843+
# Replace the name of the linked object with the name of this object
1844+
hover_str = link_msg.replace(self.link_obj.name, self.name, 1)
1845+
if isinstance(link_docs, str):
1846+
# Get just the docstring of the link, if any, no args
1847+
link_doc_top = self.link_obj.get_documentation()
1848+
# Replace the linked objects topmost documentation with the
1849+
# documentation of the procedure pointer if one is present
1850+
if link_doc_top is not None:
1851+
docs = link_docs.replace(link_doc_top, docs, 1)
1852+
# If no top docstring is present at the linked object but there
1853+
# are docstrings for the arguments, add them to the end of the
1854+
# documentation for this object
1855+
elif link_docs:
1856+
docs += " \n" + link_docs
1857+
return hover_str, docs, True
18451858

18461859
def get_signature(self, drop_arg=-1):
18471860
if self.link_obj is not None:

0 commit comments

Comments
 (0)