Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/hover-functions #49

Merged
merged 11 commits into from
Feb 22, 2022
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
# CHANGELONG

## 2.2.2

### Changed

- Changed the way function hover messages are displayed, now signatures are standardised
([gnikit/fortls#47](https://github.com/gnikit/fortls/issues/47))

### Fixed

- Fixed hovering over functions displaying as theire result types
([gnikit/fortls#22](https://github.com/gnikit/fortls/issues/22))
- Fixed function modifiers not displaying upon hover
([gnikit/fortls#48](https://github.com/gnikit/fortls/issues/48))
- Fixed function hover when returning arrays
([gnikit/fortls#50](https://github.com/gnikit/fortls/issues/50))

## 2.2.1

### Changed
Expand Down
23 changes: 23 additions & 0 deletions fortls/constants.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from __future__ import annotations

import logging
import sys
from dataclasses import dataclass, field

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

Expand Down Expand Up @@ -58,3 +61,23 @@
# it cannot also be a comment that requires !, c, d
# and ^= (xor_eq) operator is invalid in Fortran C++ preproc
FORTRAN_LITERAL = "0^=__LITERAL_INTERNAL_DUMMY_VAR_"


@dataclass
class RESULT_sig:
name: str = field(default=None)
type: str = field(default=None)
keywords: list[str] = field(default_factory=list)


@dataclass
class FUN_sig:
name: str
args: str
keywords: list[str] = field(default_factory=list)
mod_flag: bool = field(default=False)
result: RESULT_sig = field(default_factory=RESULT_sig)

def __post_init__(self):
if not self.result.name:
self.result.name = self.name
2 changes: 1 addition & 1 deletion fortls/helper_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ def set_keyword_ordering(sorted):
sort_keywords = sorted


def map_keywords(keywords):
def map_keywords(keywords: list[str]):
mapped_keywords = []
keyword_info = {}
for keyword in keywords:
Expand Down
4 changes: 3 additions & 1 deletion fortls/intrinsics.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,9 @@ def create_object(json_obj, enc_obj=None):
0,
name,
args=args,
return_type=[json_obj["return"], keywords, keyword_info],
result_type=json_obj["return"],
keywords=keywords,
# keyword_info=keyword_info,
)
elif json_obj["type"] == 3:
return fortran_var(
Expand Down
68 changes: 36 additions & 32 deletions fortls/langserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -740,7 +740,9 @@ def get_definition(
)
):
curr_scope = curr_scope.parent
var_obj = find_in_scope(curr_scope, def_name, self.obj_tree)
var_obj = find_in_scope(
curr_scope, def_name, self.obj_tree, var_line_number=def_line + 1
)
# Search in global scope
if var_obj is None:
if is_member:
Expand Down Expand Up @@ -881,16 +883,21 @@ def check_optional(arg, params):
req_dict = {"signatures": [signature], "activeParameter": param_num}
return req_dict

def get_all_references(self, def_obj, type_mem, file_obj=None):
def get_all_references(
self,
def_obj,
type_mem: bool,
file_obj: fortran_file = None,
):
# Search through all files
def_name = def_obj.name.lower()
def_fqsn = def_obj.FQSN
def_name: str = def_obj.name.lower()
def_fqsn: str = def_obj.FQSN
NAME_REGEX = re.compile(rf"(?:\W|^)({def_name})(?:\W|$)", re.I)
if file_obj is None:
file_set = self.workspace.items()
else:
file_set = ((file_obj.path, file_obj),)
override_cache = []
override_cache: list[str] = []
refs = {}
ref_objs = []
for filename, file_obj in file_set:
Expand All @@ -905,34 +912,31 @@ def get_all_references(self, def_obj, type_mem, file_obj=None):
continue
for match in NAME_REGEX.finditer(line):
var_def = self.get_definition(file_obj, i, match.start(1) + 1)
if var_def is not None:
ref_match = False
if (def_fqsn == var_def.FQSN) or (
var_def.FQSN in override_cache
if var_def is None:
continue
ref_match = False
if def_fqsn == var_def.FQSN or var_def.FQSN in override_cache:
ref_match = True
elif var_def.parent and var_def.parent.get_type() == CLASS_TYPE_ID:
if type_mem:
for inherit_def in var_def.parent.get_overridden(def_name):
if def_fqsn == inherit_def.FQSN:
ref_match = True
override_cache.append(var_def.FQSN)
break
if (
(var_def.sline - 1 == i)
and (var_def.file_ast.path == filename)
and (line.count("=>") == 0)
):
ref_match = True
elif var_def.parent.get_type() == CLASS_TYPE_ID:
if type_mem:
for inherit_def in var_def.parent.get_overridden(
def_name
):
if def_fqsn == inherit_def.FQSN:
ref_match = True
override_cache.append(var_def.FQSN)
break
if (
(var_def.sline - 1 == i)
and (var_def.file_ast.path == filename)
and (line.count("=>") == 0)
):
try:
if var_def.link_obj is def_obj:
ref_objs.append(var_def)
ref_match = True
except:
pass
if ref_match:
file_refs.append([i, match.start(1), match.end(1)])
try:
if var_def.link_obj is def_obj:
ref_objs.append(var_def)
ref_match = True
except:
pass
if ref_match:
file_refs.append([i, match.start(1), match.end(1)])
if len(file_refs) > 0:
refs[filename] = file_refs
return refs, ref_objs
Expand Down
126 changes: 94 additions & 32 deletions fortls/objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,9 +165,13 @@ def find_in_scope(
obj_tree: dict,
interface: bool = False,
local_only: bool = False,
var_line_number: int = None,
):
def check_scope(
local_scope: fortran_scope, var_name_lower: str, filter_public: bool = False
local_scope: fortran_scope,
var_name_lower: str,
filter_public: bool = False,
var_line_number: int = None,
):
for child in local_scope.get_children():
if child.name.startswith("#GEN_INT"):
Expand All @@ -178,6 +182,19 @@ def check_scope(
if (child.vis < 0) or ((local_scope.def_vis < 0) and (child.vis <= 0)):
continue
if child.name.lower() == var_name_lower:
# For functions with an implicit result() variable the name
# of the function is used. If we are hovering over the function
# definition, we do not want the implicit result() to be returned.
# If scope is from a function and child's name is same as functions name
# and start of scope i.e. function definition is equal to the request ln
# then we are need to skip this child
if (
isinstance(local_scope, fortran_function)
and local_scope.name.lower() == child.name.lower()
and var_line_number in (local_scope.sline, local_scope.eline)
):
return None

return child
return None

Expand All @@ -186,7 +203,7 @@ def check_scope(
# Check local scope
if scope is None:
return None
tmp_var = check_scope(scope, var_name_lower)
tmp_var = check_scope(scope, var_name_lower, var_line_number=var_line_number)
if local_only or (tmp_var is not None):
return tmp_var
# Check INCLUDE statements
Expand Down Expand Up @@ -959,7 +976,7 @@ def get_hover(self, long=False, include_doc=True, drop_arg=-1):
keyword_list = get_keywords(self.keywords)
keyword_list.append(f"{self.get_desc()} ")
hover_array = [" ".join(keyword_list) + sub_sig]
self.get_docs_full(hover_array, long, include_doc, drop_arg)
hover_array = self.get_docs_full(hover_array, long, include_doc, drop_arg)
return "\n ".join(hover_array), long

def get_docs_full(
Expand All @@ -977,6 +994,7 @@ def get_docs_full(
doc_str = arg_obj.get_documentation()
if include_doc and (doc_str is not None):
hover_array += doc_str.splitlines()
return hover_array

def get_signature(self, drop_arg=-1):
arg_sigs = []
Expand Down Expand Up @@ -1070,8 +1088,8 @@ def __init__(
args: str = "",
mod_flag: bool = False,
keywords: list = None,
return_type=None,
result_var=None,
result_type: str = None,
result_name: str = None,
):
super().__init__(file_ast, line_number, name, args, mod_flag, keywords)
self.args: str = args.replace(" ", "").lower()
Expand All @@ -1080,65 +1098,108 @@ def __init__(
self.in_children: list = []
self.missing_args: list = []
self.mod_scope: bool = mod_flag
self.result_var = result_var
self.result_obj = None
self.return_type = None
if return_type is not None:
self.return_type = return_type[0]
self.result_name: str = result_name
self.result_type: str = result_type
self.result_obj: fortran_var = None
# Set the implicit result() name to be the function name
if self.result_name is None:
self.result_name = self.name

def copy_interface(self, copy_source: fortran_function):
# Call the parent class method
child_names = super().copy_interface(copy_source)
# Return specific options
self.result_var = copy_source.result_var
self.result_name = copy_source.result_name
self.result_type = copy_source.result_type
self.result_obj = copy_source.result_obj
if copy_source.result_obj is not None:
if copy_source.result_obj.name.lower() not in child_names:
self.in_children.append(copy_source.result_obj)

def resolve_link(self, obj_tree):
self.resolve_arg_link(obj_tree)
if self.result_var is not None:
result_var_lower = self.result_var.lower()
for child in self.children:
if child.name.lower() == result_var_lower:
self.result_obj = child
result_var_lower = self.result_name.lower()
for child in self.children:
if child.name.lower() == result_var_lower:
self.result_obj = child
# Update result value and type
self.result_name = child.name
self.result_type = child.get_desc()

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

def get_desc(self):
if self.result_obj is not None:
return self.result_obj.get_desc() + " FUNCTION"
if self.return_type is not None:
return self.return_type + " FUNCTION"
if self.result_type:
return self.result_type + " FUNCTION"
return "FUNCTION"

def is_callable(self):
return False

def get_hover(self, long=False, include_doc=True, drop_arg=-1):
def get_hover(
self, long: bool = False, include_doc: bool = True, drop_arg: int = -1
) -> tuple[str, bool]:
"""Construct the hover message for a FUNCTION.
Two forms are produced here the `long` i.e. the normal for hover requests

```
[MODIFIERS] FUNCTION NAME([ARGS]) RESULT(RESULT_VAR)
TYPE, [ARG_MODIFIERS] :: [ARGS]
TYPE, [RESULT_MODIFIERS] :: RESULT_VAR
```

note: intrinsic functions will display slightly different,
`RESULT_VAR` and its `TYPE` might not always be present

short form, used when functions are arguments in functions and subroutines:

```
FUNCTION NAME([ARGS]) :: ARG_LIST_NAME
```

Parameters
----------
long : bool, optional
toggle between long and short hover results, by default False
include_doc : bool, optional
if to include any documentation, by default True
drop_arg : int, optional
Ignore argument at position `drop_arg` in the argument list, by default -1

Returns
-------
tuple[str, bool]
String representative of the hover message and the `long` flag used
"""
fun_sig, _ = self.get_snippet(drop_arg=drop_arg)
fun_return = ""
if self.result_obj is not None:
fun_return, _ = self.result_obj.get_hover(include_doc=False)
if self.return_type is not None:
fun_return = self.return_type
# short hover messages do not include the result()
fun_sig += f" RESULT({self.result_name})" if long else ""
keyword_list = get_keywords(self.keywords)
keyword_list.append("FUNCTION")
hover_array = [f"{fun_return} {' '.join(keyword_list)} {fun_sig}"]
self.get_docs_full(hover_array, long, include_doc, drop_arg)

hover_array = [f"{' '.join(keyword_list)} {fun_sig}"]
hover_array = self.get_docs_full(hover_array, long, include_doc, drop_arg)
# Only append the return value if using long form
if self.result_obj and long:
arg_doc, _ = self.result_obj.get_hover(include_doc=False)
hover_array.append(f"{arg_doc} :: {self.result_obj.name}")
# intrinsic functions, where the return type is missing but can be inferred
elif self.result_type and long:
# prepend type to function signature
hover_array[0] = f"{self.result_type} {hover_array[0]}"
return "\n ".join(hover_array), long

def get_interface(self, name_replace=None, change_arg=-1, change_strings=None):
fun_sig, _ = self.get_snippet(name_replace=name_replace)
fun_sig += f" RESULT({self.result_name})"
# XXX:
keyword_list = []
if self.return_type is not None:
keyword_list.append(self.return_type)
if self.result_obj is not None:
fun_sig += f" RESULT({self.result_obj.name})"
if self.result_type:
keyword_list.append(self.result_type)
keyword_list += get_keywords(self.keywords)
keyword_list.append("FUNCTION ")

interface_array = self.get_interface_array(
keyword_list, fun_sig, change_arg, change_strings
)
Expand Down Expand Up @@ -1628,6 +1689,7 @@ def get_hover(self, long=False, include_doc=True, drop_arg=-1):
hover_str = ", ".join(
[self.desc] + get_keywords(self.keywords, self.keyword_info)
)
# TODO: at this stage we can mae this lowercase
# Add parameter value in the output
if self.is_parameter() and self.param_val:
hover_str += f" :: {self.name} = {self.param_val}"
Expand Down
Loading