Skip to content

Commit

Permalink
Merge pull request #11 from gnikit/feature/hover_improvements
Browse files Browse the repository at this point in the history
Hover improvements
  • Loading branch information
gnikit authored Jan 3, 2022
2 parents 0e9348a + 8bf1d23 commit ebfe243
Show file tree
Hide file tree
Showing 12 changed files with 456 additions and 304 deletions.
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# CHANGELONG

## 1.16.0

### Adds

- Adds value for `PARAMETER` variables on hover
([#116](https://github.com/hansec/fortran-language-server/issues/116))
([gnikit/fortls#1](https://github.com/gnikit/fortls/issues/1))

## 1.15.2

### Fixes

- Further improves the literal variable hover added in v1.14.0

## 1.15.1

### Fixes
Expand Down
3 changes: 2 additions & 1 deletion fortls/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@
from multiprocessing import freeze_support

from ._version import __version__
from .helper_functions import resolve_globs, only_dirs
from .jsonrpc import JSONRPC2Connection, ReadWriter, path_from_uri
from .langserver import LangServer, resolve_globs, only_dirs
from .langserver import LangServer
from .parse_fortran import fortran_file, process_file


Expand Down
53 changes: 53 additions & 0 deletions fortls/constants.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,56 @@
import sys

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

# Global variables
sort_keywords = True

# Keyword identifiers
KEYWORD_LIST = [
"pointer",
"allocatable",
"optional",
"public",
"private",
"nopass",
"target",
"save",
"parameter",
"contiguous",
"deferred",
"dimension",
"intent",
"pass",
"pure",
"impure",
"elemental",
"recursive",
"abstract",
]
KEYWORD_ID_DICT = {keyword: ind for (ind, keyword) in enumerate(KEYWORD_LIST)}

# Type identifiers
BASE_TYPE_ID = -1
MODULE_TYPE_ID = 1
SUBROUTINE_TYPE_ID = 2
FUNCTION_TYPE_ID = 3
CLASS_TYPE_ID = 4
INTERFACE_TYPE_ID = 5
VAR_TYPE_ID = 6
METH_TYPE_ID = 7
SUBMODULE_TYPE_ID = 8
BLOCK_TYPE_ID = 9
SELECT_TYPE_ID = 10
DO_TYPE_ID = 11
WHERE_TYPE_ID = 12
IF_TYPE_ID = 13
ASSOC_TYPE_ID = 14
ENUM_TYPE_ID = 15

# A string used to mark literals e.g. 10, 3.14, "words", etc.
# The description name chosen is non-ambiguous and cannot naturally
# occur in Fortran (with/out C preproc) code
# It is invalid syntax to define a type starting with numerics
# 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_"
218 changes: 218 additions & 0 deletions fortls/helper_functions.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
from __future__ import annotations

import logging
import os
from pathlib import Path

from fortls.constants import KEYWORD_ID_DICT, KEYWORD_LIST, sort_keywords
from fortls.regex_patterns import (
DQ_STRING_REGEX,
FIXED_COMMENT_LINE_MATCH,
Expand All @@ -6,10 +13,13 @@
LOGICAL_REGEX,
NAT_VAR_REGEX,
NUMBER_REGEX,
OBJBREAK_REGEX,
SQ_STRING_REGEX,
WORD_REGEX,
)

log = logging.getLogger(__name__)


def expand_name(line, char_poss):
"""Get full word containing given cursor position"""
Expand Down Expand Up @@ -124,3 +134,211 @@ def find_paren_match(test_str):
if paren_count == 0:
return i
return ind


def get_line_prefix(pre_lines: list, curr_line: str, col: int, qs: bool = True) -> str:
"""Get code line prefix from current line and preceding continuation lines
Parameters
----------
pre_lines : list
for multiline cases get all the previous, relevant lines
curr_line : str
the current line
col : int
column index of the current line
qs : bool, optional
strip quotes i.e. string literals from `curr_line` and `pre_lines`.
Need this disable when hovering over string literals, by default True
Returns
-------
str
part of the line including any relevant line continuations before `col`
"""
if (curr_line is None) or (col > len(curr_line)) or (curr_line.startswith("#")):
return None
prepend_string = "".join(pre_lines)
curr_line = prepend_string + curr_line
col += len(prepend_string)
line_prefix = curr_line[:col].lower()
# Ignore string literals
if qs:
if (line_prefix.find("'") > -1) or (line_prefix.find('"') > -1):
sq_count = 0
dq_count = 0
for char in line_prefix:
if (char == "'") and (dq_count % 2 == 0):
sq_count += 1
elif (char == '"') and (sq_count % 2 == 0):
dq_count += 1
if (dq_count % 2 == 1) or (sq_count % 2 == 1):
return None
return line_prefix


def resolve_globs(glob_path: str, root_path: str = None) -> list[str]:
"""Resolve glob patterns
Parameters
----------
glob_path : str
Path containing the glob pattern follows
`fnmatch` glob pattern, can include relative paths, etc.
see fnmatch: https://docs.python.org/3/library/fnmatch.html#module-fnmatch
root_path : str, optional
root path to start glob search. If left empty the root_path will be
extracted from the glob_path, by default None
Returns
-------
list[str]
Expanded glob patterns with absolute paths.
Absolute paths are used to resolve any potential ambiguity
"""
# Path.glob returns a generator, we then cast the Path obj to a str
# alternatively use p.as_posix()
if root_path:
return [str(p) for p in Path(root_path).resolve().glob(glob_path)]
# Attempt to extract the root and glob pattern from the glob_path
# This is substantially less robust that then above
else:
p = Path(glob_path).expanduser()
parts = p.parts[p.is_absolute() :]
return [str(i) for i in Path(p.root).resolve().glob(str(Path(*parts)))]


def only_dirs(paths: list[str], err_msg: list = []) -> list[str]:
dirs: list[str] = []
for p in paths:
if os.path.isdir(p):
dirs.append(p)
elif os.path.isfile(p):
continue
else:
msg: str = (
f"Directory '{p}' specified in Configuration settings file does not"
" exist"
)
if err_msg:
err_msg.append([2, msg])
else:
log.warning(msg)
return dirs


def set_keyword_ordering(sorted):
global sort_keywords
sort_keywords = sorted


def map_keywords(keywords):
mapped_keywords = []
keyword_info = {}
for keyword in keywords:
keyword_prefix = keyword.split("(")[0].lower()
keyword_ind = KEYWORD_ID_DICT.get(keyword_prefix)
if keyword_ind is not None:
mapped_keywords.append(keyword_ind)
if keyword_prefix in ("intent", "dimension", "pass"):
keyword_substring = get_paren_substring(keyword)
if keyword_substring is not None:
keyword_info[keyword_prefix] = keyword_substring
if sort_keywords:
mapped_keywords.sort()
return mapped_keywords, keyword_info


def get_keywords(keywords, keyword_info={}):
keyword_strings = []
for keyword_id in keywords:
string_rep = KEYWORD_LIST[keyword_id]
addl_info = keyword_info.get(string_rep)
string_rep = string_rep.upper()
if addl_info is not None:
string_rep += "({0})".format(addl_info)
keyword_strings.append(string_rep)
return keyword_strings


def get_paren_substring(test_str):
i1 = test_str.find("(")
i2 = test_str.rfind(")")
if i1 > -1 and i2 > i1:
return test_str[i1 + 1 : i2]
else:
return None


def get_paren_level(line):
"""Get sub-string corresponding to a single parenthesis level,
via backward search up through the line.
Examples:
"CALL sub1(arg1,arg2" -> ("arg1,arg2", [[10, 19]])
"CALL sub1(arg1(i),arg2" -> ("arg1,arg2", [[10, 14], [17, 22]])
"""
if line == "":
return "", [[0, 0]]
level = 0
in_string = False
string_char = ""
i1 = len(line)
sections = []
for i in range(len(line) - 1, -1, -1):
char = line[i]
if in_string:
if char == string_char:
in_string = False
continue
if (char == "(") or (char == "["):
level -= 1
if level == 0:
i1 = i
elif level < 0:
sections.append([i + 1, i1])
break
elif (char == ")") or (char == "]"):
level += 1
if level == 1:
sections.append([i + 1, i1])
elif (char == "'") or (char == '"'):
in_string = True
string_char = char
if level == 0:
sections.append([i, i1])
sections.reverse()
out_string = ""
for section in sections:
out_string += line[section[0] : section[1]]
return out_string, sections


def get_var_stack(line):
"""Get user-defined type field sequence terminating the given line
Examples:
"myvar%foo%bar" -> ["myvar", "foo", "bar"]
"myarray(i)%foo%bar" -> ["myarray", "foo", "bar"]
"CALL self%method(this%foo" -> ["this", "foo"]
"""
if len(line) == 0:
return [""]
final_var, sections = get_paren_level(line)
if final_var == "":
return [""]
# Continuation of variable after paren requires '%' character
iLast = 0
for (i, section) in enumerate(sections):
if not line[section[0] : section[1]].startswith("%"):
iLast = i
final_var = ""
for section in sections[iLast:]:
final_var += line[section[0] : section[1]]
#
if final_var is not None:
final_op_split = OBJBREAK_REGEX.split(final_var)
return final_op_split[-1].split("%")
else:
return None
2 changes: 1 addition & 1 deletion fortls/intrinsics.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import json
import os

from fortls.helper_functions import map_keywords
from fortls.objects import (
fortran_ast,
fortran_function,
Expand All @@ -9,7 +10,6 @@
fortran_subroutine,
fortran_type,
fortran_var,
map_keywords,
)

none_ast = fortran_ast()
Expand Down
Loading

0 comments on commit ebfe243

Please sign in to comment.