Skip to content

Commit

Permalink
Merge pull request #49 from gnikit/feature/hover-functions
Browse files Browse the repository at this point in the history
Feature/hover-functions
  • Loading branch information
gnikit authored Feb 22, 2022
2 parents b0987da + 8aa6478 commit 914ff47
Show file tree
Hide file tree
Showing 13 changed files with 403 additions and 107 deletions.
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

0 comments on commit 914ff47

Please sign in to comment.