Skip to content

Commit 914ff47

Browse files
authored
Merge pull request #49 from gnikit/feature/hover-functions
Feature/hover-functions
2 parents b0987da + 8aa6478 commit 914ff47

13 files changed

+403
-107
lines changed

CHANGELOG.md

+16
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,21 @@
11
# CHANGELONG
22

3+
## 2.2.2
4+
5+
### Changed
6+
7+
- Changed the way function hover messages are displayed, now signatures are standardised
8+
([gnikit/fortls#47](https://github.com/gnikit/fortls/issues/47))
9+
10+
### Fixed
11+
12+
- Fixed hovering over functions displaying as theire result types
13+
([gnikit/fortls#22](https://github.com/gnikit/fortls/issues/22))
14+
- Fixed function modifiers not displaying upon hover
15+
([gnikit/fortls#48](https://github.com/gnikit/fortls/issues/48))
16+
- Fixed function hover when returning arrays
17+
([gnikit/fortls#50](https://github.com/gnikit/fortls/issues/50))
18+
319
## 2.2.1
420

521
### Changed

fortls/constants.py

+23
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
from __future__ import annotations
2+
13
import logging
24
import sys
5+
from dataclasses import dataclass, field
36

47
PY3K = sys.version_info >= (3, 0)
58

@@ -58,3 +61,23 @@
5861
# it cannot also be a comment that requires !, c, d
5962
# and ^= (xor_eq) operator is invalid in Fortran C++ preproc
6063
FORTRAN_LITERAL = "0^=__LITERAL_INTERNAL_DUMMY_VAR_"
64+
65+
66+
@dataclass
67+
class RESULT_sig:
68+
name: str = field(default=None)
69+
type: str = field(default=None)
70+
keywords: list[str] = field(default_factory=list)
71+
72+
73+
@dataclass
74+
class FUN_sig:
75+
name: str
76+
args: str
77+
keywords: list[str] = field(default_factory=list)
78+
mod_flag: bool = field(default=False)
79+
result: RESULT_sig = field(default_factory=RESULT_sig)
80+
81+
def __post_init__(self):
82+
if not self.result.name:
83+
self.result.name = self.name

fortls/helper_functions.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,7 @@ def set_keyword_ordering(sorted):
311311
sort_keywords = sorted
312312

313313

314-
def map_keywords(keywords):
314+
def map_keywords(keywords: list[str]):
315315
mapped_keywords = []
316316
keyword_info = {}
317317
for keyword in keywords:

fortls/intrinsics.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,9 @@ def create_object(json_obj, enc_obj=None):
117117
0,
118118
name,
119119
args=args,
120-
return_type=[json_obj["return"], keywords, keyword_info],
120+
result_type=json_obj["return"],
121+
keywords=keywords,
122+
# keyword_info=keyword_info,
121123
)
122124
elif json_obj["type"] == 3:
123125
return fortran_var(

fortls/langserver.py

+36-32
Original file line numberDiff line numberDiff line change
@@ -740,7 +740,9 @@ def get_definition(
740740
)
741741
):
742742
curr_scope = curr_scope.parent
743-
var_obj = find_in_scope(curr_scope, def_name, self.obj_tree)
743+
var_obj = find_in_scope(
744+
curr_scope, def_name, self.obj_tree, var_line_number=def_line + 1
745+
)
744746
# Search in global scope
745747
if var_obj is None:
746748
if is_member:
@@ -881,16 +883,21 @@ def check_optional(arg, params):
881883
req_dict = {"signatures": [signature], "activeParameter": param_num}
882884
return req_dict
883885

884-
def get_all_references(self, def_obj, type_mem, file_obj=None):
886+
def get_all_references(
887+
self,
888+
def_obj,
889+
type_mem: bool,
890+
file_obj: fortran_file = None,
891+
):
885892
# Search through all files
886-
def_name = def_obj.name.lower()
887-
def_fqsn = def_obj.FQSN
893+
def_name: str = def_obj.name.lower()
894+
def_fqsn: str = def_obj.FQSN
888895
NAME_REGEX = re.compile(rf"(?:\W|^)({def_name})(?:\W|$)", re.I)
889896
if file_obj is None:
890897
file_set = self.workspace.items()
891898
else:
892899
file_set = ((file_obj.path, file_obj),)
893-
override_cache = []
900+
override_cache: list[str] = []
894901
refs = {}
895902
ref_objs = []
896903
for filename, file_obj in file_set:
@@ -905,34 +912,31 @@ def get_all_references(self, def_obj, type_mem, file_obj=None):
905912
continue
906913
for match in NAME_REGEX.finditer(line):
907914
var_def = self.get_definition(file_obj, i, match.start(1) + 1)
908-
if var_def is not None:
909-
ref_match = False
910-
if (def_fqsn == var_def.FQSN) or (
911-
var_def.FQSN in override_cache
915+
if var_def is None:
916+
continue
917+
ref_match = False
918+
if def_fqsn == var_def.FQSN or var_def.FQSN in override_cache:
919+
ref_match = True
920+
elif var_def.parent and var_def.parent.get_type() == CLASS_TYPE_ID:
921+
if type_mem:
922+
for inherit_def in var_def.parent.get_overridden(def_name):
923+
if def_fqsn == inherit_def.FQSN:
924+
ref_match = True
925+
override_cache.append(var_def.FQSN)
926+
break
927+
if (
928+
(var_def.sline - 1 == i)
929+
and (var_def.file_ast.path == filename)
930+
and (line.count("=>") == 0)
912931
):
913-
ref_match = True
914-
elif var_def.parent.get_type() == CLASS_TYPE_ID:
915-
if type_mem:
916-
for inherit_def in var_def.parent.get_overridden(
917-
def_name
918-
):
919-
if def_fqsn == inherit_def.FQSN:
920-
ref_match = True
921-
override_cache.append(var_def.FQSN)
922-
break
923-
if (
924-
(var_def.sline - 1 == i)
925-
and (var_def.file_ast.path == filename)
926-
and (line.count("=>") == 0)
927-
):
928-
try:
929-
if var_def.link_obj is def_obj:
930-
ref_objs.append(var_def)
931-
ref_match = True
932-
except:
933-
pass
934-
if ref_match:
935-
file_refs.append([i, match.start(1), match.end(1)])
932+
try:
933+
if var_def.link_obj is def_obj:
934+
ref_objs.append(var_def)
935+
ref_match = True
936+
except:
937+
pass
938+
if ref_match:
939+
file_refs.append([i, match.start(1), match.end(1)])
936940
if len(file_refs) > 0:
937941
refs[filename] = file_refs
938942
return refs, ref_objs

fortls/objects.py

+94-32
Original file line numberDiff line numberDiff line change
@@ -165,9 +165,13 @@ def find_in_scope(
165165
obj_tree: dict,
166166
interface: bool = False,
167167
local_only: bool = False,
168+
var_line_number: int = None,
168169
):
169170
def check_scope(
170-
local_scope: fortran_scope, var_name_lower: str, filter_public: bool = False
171+
local_scope: fortran_scope,
172+
var_name_lower: str,
173+
filter_public: bool = False,
174+
var_line_number: int = None,
171175
):
172176
for child in local_scope.get_children():
173177
if child.name.startswith("#GEN_INT"):
@@ -178,6 +182,19 @@ def check_scope(
178182
if (child.vis < 0) or ((local_scope.def_vis < 0) and (child.vis <= 0)):
179183
continue
180184
if child.name.lower() == var_name_lower:
185+
# For functions with an implicit result() variable the name
186+
# of the function is used. If we are hovering over the function
187+
# definition, we do not want the implicit result() to be returned.
188+
# If scope is from a function and child's name is same as functions name
189+
# and start of scope i.e. function definition is equal to the request ln
190+
# then we are need to skip this child
191+
if (
192+
isinstance(local_scope, fortran_function)
193+
and local_scope.name.lower() == child.name.lower()
194+
and var_line_number in (local_scope.sline, local_scope.eline)
195+
):
196+
return None
197+
181198
return child
182199
return None
183200

@@ -186,7 +203,7 @@ def check_scope(
186203
# Check local scope
187204
if scope is None:
188205
return None
189-
tmp_var = check_scope(scope, var_name_lower)
206+
tmp_var = check_scope(scope, var_name_lower, var_line_number=var_line_number)
190207
if local_only or (tmp_var is not None):
191208
return tmp_var
192209
# Check INCLUDE statements
@@ -959,7 +976,7 @@ def get_hover(self, long=False, include_doc=True, drop_arg=-1):
959976
keyword_list = get_keywords(self.keywords)
960977
keyword_list.append(f"{self.get_desc()} ")
961978
hover_array = [" ".join(keyword_list) + sub_sig]
962-
self.get_docs_full(hover_array, long, include_doc, drop_arg)
979+
hover_array = self.get_docs_full(hover_array, long, include_doc, drop_arg)
963980
return "\n ".join(hover_array), long
964981

965982
def get_docs_full(
@@ -977,6 +994,7 @@ def get_docs_full(
977994
doc_str = arg_obj.get_documentation()
978995
if include_doc and (doc_str is not None):
979996
hover_array += doc_str.splitlines()
997+
return hover_array
980998

981999
def get_signature(self, drop_arg=-1):
9821000
arg_sigs = []
@@ -1070,8 +1088,8 @@ def __init__(
10701088
args: str = "",
10711089
mod_flag: bool = False,
10721090
keywords: list = None,
1073-
return_type=None,
1074-
result_var=None,
1091+
result_type: str = None,
1092+
result_name: str = None,
10751093
):
10761094
super().__init__(file_ast, line_number, name, args, mod_flag, keywords)
10771095
self.args: str = args.replace(" ", "").lower()
@@ -1080,65 +1098,108 @@ def __init__(
10801098
self.in_children: list = []
10811099
self.missing_args: list = []
10821100
self.mod_scope: bool = mod_flag
1083-
self.result_var = result_var
1084-
self.result_obj = None
1085-
self.return_type = None
1086-
if return_type is not None:
1087-
self.return_type = return_type[0]
1101+
self.result_name: str = result_name
1102+
self.result_type: str = result_type
1103+
self.result_obj: fortran_var = None
1104+
# Set the implicit result() name to be the function name
1105+
if self.result_name is None:
1106+
self.result_name = self.name
10881107

10891108
def copy_interface(self, copy_source: fortran_function):
10901109
# Call the parent class method
10911110
child_names = super().copy_interface(copy_source)
10921111
# Return specific options
1093-
self.result_var = copy_source.result_var
1112+
self.result_name = copy_source.result_name
1113+
self.result_type = copy_source.result_type
10941114
self.result_obj = copy_source.result_obj
10951115
if copy_source.result_obj is not None:
10961116
if copy_source.result_obj.name.lower() not in child_names:
10971117
self.in_children.append(copy_source.result_obj)
10981118

10991119
def resolve_link(self, obj_tree):
11001120
self.resolve_arg_link(obj_tree)
1101-
if self.result_var is not None:
1102-
result_var_lower = self.result_var.lower()
1103-
for child in self.children:
1104-
if child.name.lower() == result_var_lower:
1105-
self.result_obj = child
1121+
result_var_lower = self.result_name.lower()
1122+
for child in self.children:
1123+
if child.name.lower() == result_var_lower:
1124+
self.result_obj = child
1125+
# Update result value and type
1126+
self.result_name = child.name
1127+
self.result_type = child.get_desc()
11061128

11071129
def get_type(self, no_link=False):
11081130
return FUNCTION_TYPE_ID
11091131

11101132
def get_desc(self):
1111-
if self.result_obj is not None:
1112-
return self.result_obj.get_desc() + " FUNCTION"
1113-
if self.return_type is not None:
1114-
return self.return_type + " FUNCTION"
1133+
if self.result_type:
1134+
return self.result_type + " FUNCTION"
11151135
return "FUNCTION"
11161136

11171137
def is_callable(self):
11181138
return False
11191139

1120-
def get_hover(self, long=False, include_doc=True, drop_arg=-1):
1140+
def get_hover(
1141+
self, long: bool = False, include_doc: bool = True, drop_arg: int = -1
1142+
) -> tuple[str, bool]:
1143+
"""Construct the hover message for a FUNCTION.
1144+
Two forms are produced here the `long` i.e. the normal for hover requests
1145+
1146+
```
1147+
[MODIFIERS] FUNCTION NAME([ARGS]) RESULT(RESULT_VAR)
1148+
TYPE, [ARG_MODIFIERS] :: [ARGS]
1149+
TYPE, [RESULT_MODIFIERS] :: RESULT_VAR
1150+
```
1151+
1152+
note: intrinsic functions will display slightly different,
1153+
`RESULT_VAR` and its `TYPE` might not always be present
1154+
1155+
short form, used when functions are arguments in functions and subroutines:
1156+
1157+
```
1158+
FUNCTION NAME([ARGS]) :: ARG_LIST_NAME
1159+
```
1160+
1161+
Parameters
1162+
----------
1163+
long : bool, optional
1164+
toggle between long and short hover results, by default False
1165+
include_doc : bool, optional
1166+
if to include any documentation, by default True
1167+
drop_arg : int, optional
1168+
Ignore argument at position `drop_arg` in the argument list, by default -1
1169+
1170+
Returns
1171+
-------
1172+
tuple[str, bool]
1173+
String representative of the hover message and the `long` flag used
1174+
"""
11211175
fun_sig, _ = self.get_snippet(drop_arg=drop_arg)
1122-
fun_return = ""
1123-
if self.result_obj is not None:
1124-
fun_return, _ = self.result_obj.get_hover(include_doc=False)
1125-
if self.return_type is not None:
1126-
fun_return = self.return_type
1176+
# short hover messages do not include the result()
1177+
fun_sig += f" RESULT({self.result_name})" if long else ""
11271178
keyword_list = get_keywords(self.keywords)
11281179
keyword_list.append("FUNCTION")
1129-
hover_array = [f"{fun_return} {' '.join(keyword_list)} {fun_sig}"]
1130-
self.get_docs_full(hover_array, long, include_doc, drop_arg)
1180+
1181+
hover_array = [f"{' '.join(keyword_list)} {fun_sig}"]
1182+
hover_array = self.get_docs_full(hover_array, long, include_doc, drop_arg)
1183+
# Only append the return value if using long form
1184+
if self.result_obj and long:
1185+
arg_doc, _ = self.result_obj.get_hover(include_doc=False)
1186+
hover_array.append(f"{arg_doc} :: {self.result_obj.name}")
1187+
# intrinsic functions, where the return type is missing but can be inferred
1188+
elif self.result_type and long:
1189+
# prepend type to function signature
1190+
hover_array[0] = f"{self.result_type} {hover_array[0]}"
11311191
return "\n ".join(hover_array), long
11321192

11331193
def get_interface(self, name_replace=None, change_arg=-1, change_strings=None):
11341194
fun_sig, _ = self.get_snippet(name_replace=name_replace)
1195+
fun_sig += f" RESULT({self.result_name})"
1196+
# XXX:
11351197
keyword_list = []
1136-
if self.return_type is not None:
1137-
keyword_list.append(self.return_type)
1138-
if self.result_obj is not None:
1139-
fun_sig += f" RESULT({self.result_obj.name})"
1198+
if self.result_type:
1199+
keyword_list.append(self.result_type)
11401200
keyword_list += get_keywords(self.keywords)
11411201
keyword_list.append("FUNCTION ")
1202+
11421203
interface_array = self.get_interface_array(
11431204
keyword_list, fun_sig, change_arg, change_strings
11441205
)
@@ -1628,6 +1689,7 @@ def get_hover(self, long=False, include_doc=True, drop_arg=-1):
16281689
hover_str = ", ".join(
16291690
[self.desc] + get_keywords(self.keywords, self.keyword_info)
16301691
)
1692+
# TODO: at this stage we can mae this lowercase
16311693
# Add parameter value in the output
16321694
if self.is_parameter() and self.param_val:
16331695
hover_str += f" :: {self.name} = {self.param_val}"

0 commit comments

Comments
 (0)