Skip to content

Commit ebfe243

Browse files
authored
Merge pull request #11 from gnikit/feature/hover_improvements
Hover improvements
2 parents 0e9348a + 8bf1d23 commit ebfe243

File tree

12 files changed

+456
-304
lines changed

12 files changed

+456
-304
lines changed

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
# CHANGELONG
22

3+
## 1.16.0
4+
5+
### Adds
6+
7+
- Adds value for `PARAMETER` variables on hover
8+
([#116](https://github.com/hansec/fortran-language-server/issues/116))
9+
([gnikit/fortls#1](https://github.com/gnikit/fortls/issues/1))
10+
11+
## 1.15.2
12+
13+
### Fixes
14+
15+
- Further improves the literal variable hover added in v1.14.0
16+
317
## 1.15.1
418

519
### Fixes

fortls/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88
from multiprocessing import freeze_support
99

1010
from ._version import __version__
11+
from .helper_functions import resolve_globs, only_dirs
1112
from .jsonrpc import JSONRPC2Connection, ReadWriter, path_from_uri
12-
from .langserver import LangServer, resolve_globs, only_dirs
13+
from .langserver import LangServer
1314
from .parse_fortran import fortran_file, process_file
1415

1516

fortls/constants.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,56 @@
11
import sys
22

33
PY3K = sys.version_info >= (3, 0)
4+
5+
# Global variables
6+
sort_keywords = True
7+
8+
# Keyword identifiers
9+
KEYWORD_LIST = [
10+
"pointer",
11+
"allocatable",
12+
"optional",
13+
"public",
14+
"private",
15+
"nopass",
16+
"target",
17+
"save",
18+
"parameter",
19+
"contiguous",
20+
"deferred",
21+
"dimension",
22+
"intent",
23+
"pass",
24+
"pure",
25+
"impure",
26+
"elemental",
27+
"recursive",
28+
"abstract",
29+
]
30+
KEYWORD_ID_DICT = {keyword: ind for (ind, keyword) in enumerate(KEYWORD_LIST)}
31+
32+
# Type identifiers
33+
BASE_TYPE_ID = -1
34+
MODULE_TYPE_ID = 1
35+
SUBROUTINE_TYPE_ID = 2
36+
FUNCTION_TYPE_ID = 3
37+
CLASS_TYPE_ID = 4
38+
INTERFACE_TYPE_ID = 5
39+
VAR_TYPE_ID = 6
40+
METH_TYPE_ID = 7
41+
SUBMODULE_TYPE_ID = 8
42+
BLOCK_TYPE_ID = 9
43+
SELECT_TYPE_ID = 10
44+
DO_TYPE_ID = 11
45+
WHERE_TYPE_ID = 12
46+
IF_TYPE_ID = 13
47+
ASSOC_TYPE_ID = 14
48+
ENUM_TYPE_ID = 15
49+
50+
# A string used to mark literals e.g. 10, 3.14, "words", etc.
51+
# The description name chosen is non-ambiguous and cannot naturally
52+
# occur in Fortran (with/out C preproc) code
53+
# It is invalid syntax to define a type starting with numerics
54+
# it cannot also be a comment that requires !, c, d
55+
# and ^= (xor_eq) operator is invalid in Fortran C++ preproc
56+
FORTRAN_LITERAL = "0^=__LITERAL_INTERNAL_DUMMY_VAR_"

fortls/helper_functions.py

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
from __future__ import annotations
2+
3+
import logging
4+
import os
5+
from pathlib import Path
6+
7+
from fortls.constants import KEYWORD_ID_DICT, KEYWORD_LIST, sort_keywords
18
from fortls.regex_patterns import (
29
DQ_STRING_REGEX,
310
FIXED_COMMENT_LINE_MATCH,
@@ -6,10 +13,13 @@
613
LOGICAL_REGEX,
714
NAT_VAR_REGEX,
815
NUMBER_REGEX,
16+
OBJBREAK_REGEX,
917
SQ_STRING_REGEX,
1018
WORD_REGEX,
1119
)
1220

21+
log = logging.getLogger(__name__)
22+
1323

1424
def expand_name(line, char_poss):
1525
"""Get full word containing given cursor position"""
@@ -124,3 +134,211 @@ def find_paren_match(test_str):
124134
if paren_count == 0:
125135
return i
126136
return ind
137+
138+
139+
def get_line_prefix(pre_lines: list, curr_line: str, col: int, qs: bool = True) -> str:
140+
"""Get code line prefix from current line and preceding continuation lines
141+
142+
Parameters
143+
----------
144+
pre_lines : list
145+
for multiline cases get all the previous, relevant lines
146+
curr_line : str
147+
the current line
148+
col : int
149+
column index of the current line
150+
qs : bool, optional
151+
strip quotes i.e. string literals from `curr_line` and `pre_lines`.
152+
Need this disable when hovering over string literals, by default True
153+
154+
Returns
155+
-------
156+
str
157+
part of the line including any relevant line continuations before `col`
158+
"""
159+
if (curr_line is None) or (col > len(curr_line)) or (curr_line.startswith("#")):
160+
return None
161+
prepend_string = "".join(pre_lines)
162+
curr_line = prepend_string + curr_line
163+
col += len(prepend_string)
164+
line_prefix = curr_line[:col].lower()
165+
# Ignore string literals
166+
if qs:
167+
if (line_prefix.find("'") > -1) or (line_prefix.find('"') > -1):
168+
sq_count = 0
169+
dq_count = 0
170+
for char in line_prefix:
171+
if (char == "'") and (dq_count % 2 == 0):
172+
sq_count += 1
173+
elif (char == '"') and (sq_count % 2 == 0):
174+
dq_count += 1
175+
if (dq_count % 2 == 1) or (sq_count % 2 == 1):
176+
return None
177+
return line_prefix
178+
179+
180+
def resolve_globs(glob_path: str, root_path: str = None) -> list[str]:
181+
"""Resolve glob patterns
182+
183+
Parameters
184+
----------
185+
glob_path : str
186+
Path containing the glob pattern follows
187+
`fnmatch` glob pattern, can include relative paths, etc.
188+
see fnmatch: https://docs.python.org/3/library/fnmatch.html#module-fnmatch
189+
190+
root_path : str, optional
191+
root path to start glob search. If left empty the root_path will be
192+
extracted from the glob_path, by default None
193+
194+
Returns
195+
-------
196+
list[str]
197+
Expanded glob patterns with absolute paths.
198+
Absolute paths are used to resolve any potential ambiguity
199+
"""
200+
# Path.glob returns a generator, we then cast the Path obj to a str
201+
# alternatively use p.as_posix()
202+
if root_path:
203+
return [str(p) for p in Path(root_path).resolve().glob(glob_path)]
204+
# Attempt to extract the root and glob pattern from the glob_path
205+
# This is substantially less robust that then above
206+
else:
207+
p = Path(glob_path).expanduser()
208+
parts = p.parts[p.is_absolute() :]
209+
return [str(i) for i in Path(p.root).resolve().glob(str(Path(*parts)))]
210+
211+
212+
def only_dirs(paths: list[str], err_msg: list = []) -> list[str]:
213+
dirs: list[str] = []
214+
for p in paths:
215+
if os.path.isdir(p):
216+
dirs.append(p)
217+
elif os.path.isfile(p):
218+
continue
219+
else:
220+
msg: str = (
221+
f"Directory '{p}' specified in Configuration settings file does not"
222+
" exist"
223+
)
224+
if err_msg:
225+
err_msg.append([2, msg])
226+
else:
227+
log.warning(msg)
228+
return dirs
229+
230+
231+
def set_keyword_ordering(sorted):
232+
global sort_keywords
233+
sort_keywords = sorted
234+
235+
236+
def map_keywords(keywords):
237+
mapped_keywords = []
238+
keyword_info = {}
239+
for keyword in keywords:
240+
keyword_prefix = keyword.split("(")[0].lower()
241+
keyword_ind = KEYWORD_ID_DICT.get(keyword_prefix)
242+
if keyword_ind is not None:
243+
mapped_keywords.append(keyword_ind)
244+
if keyword_prefix in ("intent", "dimension", "pass"):
245+
keyword_substring = get_paren_substring(keyword)
246+
if keyword_substring is not None:
247+
keyword_info[keyword_prefix] = keyword_substring
248+
if sort_keywords:
249+
mapped_keywords.sort()
250+
return mapped_keywords, keyword_info
251+
252+
253+
def get_keywords(keywords, keyword_info={}):
254+
keyword_strings = []
255+
for keyword_id in keywords:
256+
string_rep = KEYWORD_LIST[keyword_id]
257+
addl_info = keyword_info.get(string_rep)
258+
string_rep = string_rep.upper()
259+
if addl_info is not None:
260+
string_rep += "({0})".format(addl_info)
261+
keyword_strings.append(string_rep)
262+
return keyword_strings
263+
264+
265+
def get_paren_substring(test_str):
266+
i1 = test_str.find("(")
267+
i2 = test_str.rfind(")")
268+
if i1 > -1 and i2 > i1:
269+
return test_str[i1 + 1 : i2]
270+
else:
271+
return None
272+
273+
274+
def get_paren_level(line):
275+
"""Get sub-string corresponding to a single parenthesis level,
276+
via backward search up through the line.
277+
278+
Examples:
279+
"CALL sub1(arg1,arg2" -> ("arg1,arg2", [[10, 19]])
280+
"CALL sub1(arg1(i),arg2" -> ("arg1,arg2", [[10, 14], [17, 22]])
281+
"""
282+
if line == "":
283+
return "", [[0, 0]]
284+
level = 0
285+
in_string = False
286+
string_char = ""
287+
i1 = len(line)
288+
sections = []
289+
for i in range(len(line) - 1, -1, -1):
290+
char = line[i]
291+
if in_string:
292+
if char == string_char:
293+
in_string = False
294+
continue
295+
if (char == "(") or (char == "["):
296+
level -= 1
297+
if level == 0:
298+
i1 = i
299+
elif level < 0:
300+
sections.append([i + 1, i1])
301+
break
302+
elif (char == ")") or (char == "]"):
303+
level += 1
304+
if level == 1:
305+
sections.append([i + 1, i1])
306+
elif (char == "'") or (char == '"'):
307+
in_string = True
308+
string_char = char
309+
if level == 0:
310+
sections.append([i, i1])
311+
sections.reverse()
312+
out_string = ""
313+
for section in sections:
314+
out_string += line[section[0] : section[1]]
315+
return out_string, sections
316+
317+
318+
def get_var_stack(line):
319+
"""Get user-defined type field sequence terminating the given line
320+
321+
Examples:
322+
"myvar%foo%bar" -> ["myvar", "foo", "bar"]
323+
"myarray(i)%foo%bar" -> ["myarray", "foo", "bar"]
324+
"CALL self%method(this%foo" -> ["this", "foo"]
325+
"""
326+
if len(line) == 0:
327+
return [""]
328+
final_var, sections = get_paren_level(line)
329+
if final_var == "":
330+
return [""]
331+
# Continuation of variable after paren requires '%' character
332+
iLast = 0
333+
for (i, section) in enumerate(sections):
334+
if not line[section[0] : section[1]].startswith("%"):
335+
iLast = i
336+
final_var = ""
337+
for section in sections[iLast:]:
338+
final_var += line[section[0] : section[1]]
339+
#
340+
if final_var is not None:
341+
final_op_split = OBJBREAK_REGEX.split(final_var)
342+
return final_op_split[-1].split("%")
343+
else:
344+
return None

fortls/intrinsics.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import json
22
import os
33

4+
from fortls.helper_functions import map_keywords
45
from fortls.objects import (
56
fortran_ast,
67
fortran_function,
@@ -9,7 +10,6 @@
910
fortran_subroutine,
1011
fortran_type,
1112
fortran_var,
12-
map_keywords,
1313
)
1414

1515
none_ast = fortran_ast()

0 commit comments

Comments
 (0)