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

foldingRange #376

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -147,6 +147,7 @@ An example for a Configuration file is given below
"incremental_sync": true,
"lowercase_intrinsics": true,
"hover_signature": true,
"folding_range": true,
"use_signature_help": true,
"excl_paths": ["tests/**", "tools/**"],
"excl_suffixes": ["_skip.f90"],
Expand Down
2 changes: 2 additions & 0 deletions docs/options.rst
Expand Up @@ -66,6 +66,8 @@ All the ``fortls`` settings with their default arguments can be found below
"hover_signature": false,
"hover_language": "fortran90",

"folding_range": false

"max_line_length": -1,
"max_comment_line_length": -1,
"disable_diagnostics": false,
Expand Down
6 changes: 6 additions & 0 deletions fortls/fortls.schema.json
Expand Up @@ -135,6 +135,12 @@
"title": "Hover Language",
"type": "string"
},
"folding_range": {
"title": "Folding Range",
"description": "Fold editor based on language keywords",
"default": false,
"type": "boolean"
},
"max_line_length": {
"default": -1,
"description": "Maximum line length (default: -1)",
Expand Down
7 changes: 7 additions & 0 deletions fortls/interface.py
Expand Up @@ -210,6 +210,13 @@ def cli(name: str = "fortls") -> argparse.ArgumentParser:
),
)

# Folding range ------------------------------------------------------------
group.add_argument(
"--folding_range",
action="store_true",
help="Fold editor based on language keywords",
)

# Diagnostic options -------------------------------------------------------
group = parser.add_argument_group("Diagnostic options (error swigles)")
group.add_argument(
Expand Down
55 changes: 55 additions & 0 deletions fortls/langserver.py
Expand Up @@ -146,6 +146,7 @@ def noop(request: dict):
"textDocument/hover": self.serve_hover,
"textDocument/implementation": self.serve_implementation,
"textDocument/rename": self.serve_rename,
"textDocument/foldingRange": self.serve_folding_range,
"textDocument/didOpen": self.serve_onOpen,
"textDocument/didSave": self.serve_onSave,
"textDocument/didClose": self.serve_onClose,
Expand Down Expand Up @@ -226,6 +227,7 @@ def serve_initialize(self, request: dict):
"renameProvider": True,
"workspaceSymbolProvider": True,
"textDocumentSync": self.sync_type,
"foldingRangeProvider": True,
}
if self.use_signature_help:
server_capabilities["signatureHelpProvider"] = {
Expand Down Expand Up @@ -1224,6 +1226,56 @@ def serve_rename(self, request: dict):
)
return {"changes": changes}

def serve_folding_range(self, request: dict):
# Get parameters from request
params: dict = request["params"]
uri: str = params["textDocument"]["uri"]
path = path_from_uri(uri)
# Find object
file_obj = self.workspace.get(path)
if file_obj is None:
return None
if file_obj.ast is None:
return None
else:
folding_start = file_obj.ast.folding_start
folding_end = file_obj.ast.folding_end
if (
folding_start is None
or folding_end is None
or len(folding_start) != len(folding_end)
):
return None
# Construct folding_rage list
folding_ranges = []
# First treating scope objects...
for scope in file_obj.ast.scope_list:
n_mlines = len(scope.mlines)
# ...with intermediate folding lines (if, select)...
if n_mlines > 0:
self.add_range(folding_ranges, scope.sline - 1, scope.mlines[0] - 2)
for i in range(1, n_mlines):
self.add_range(
folding_ranges, scope.mlines[i - 1] - 1, scope.mlines[i] - 2
)
self.add_range(folding_ranges, scope.mlines[-1] - 1, scope.eline - 2)
# ...and without
else:
self.add_range(folding_ranges, scope.sline - 1, scope.eline - 2)
# Then treat comment blocks
folds = len(folding_start)
for i in range(0, folds):
self.add_range(folding_ranges, folding_start[i] - 1, folding_end[i] - 1)

return folding_ranges

def add_range(self, folding_ranges: list, start: int, end: int):
folding_range = {
"startLine": start,
"endLine": end,
}
folding_ranges.append(folding_range)

def serve_codeActions(self, request: dict):
params: dict = request["params"]
uri: str = params["textDocument"]["uri"]
Expand Down Expand Up @@ -1621,6 +1673,9 @@ def _load_config_file_general(self, config_dict: dict) -> None:
self.hover_signature = config_dict.get("hover_signature", self.hover_signature)
self.hover_language = config_dict.get("hover_language", self.hover_language)

# Folding range --------------------------------------------------------
self.folding_range = config_dict.get("folding_range", self.folding_range)

# Diagnostic options ---------------------------------------------------
self.max_line_length = config_dict.get("max_line_length", self.max_line_length)
self.max_comment_line_length = config_dict.get(
Expand Down
4 changes: 4 additions & 0 deletions fortls/parsers/internal/ast.py
Expand Up @@ -36,6 +36,10 @@ def __init__(self, file_obj=None):
self.inherit_objs: list = []
self.linkable_objs: list = []
self.external_objs: list = []
self.folding_start: list = []
self.folding_end: list = []
self.comment_block_start = 0
self.comment_block_end = 0
self.none_scope = None
self.inc_scope = None
self.current_scope = None
Expand Down
25 changes: 25 additions & 0 deletions fortls/parsers/internal/parser.py
Expand Up @@ -1306,6 +1306,20 @@ def parse(
line = multi_lines.pop()
get_full = False

# Add comment blocks to folding patterns
if FRegex.FREE_COMMENT.match(line) is not None:
if file_ast.comment_block_start == 0:
file_ast.comment_block_start = line_no
else:
file_ast.comment_block_end = line_no
elif file_ast.comment_block_start != 0:
# Only fold consecutive comment lines
if file_ast.comment_block_end > file_ast.comment_block_start + 1:
file_ast.folding_start.append(file_ast.comment_block_start)
file_ast.folding_end.append(line_no - 1)
file_ast.comment_block_end = 0
file_ast.comment_block_start = 0

if line == "":
continue # Skip empty lines

Expand Down Expand Up @@ -1351,8 +1365,19 @@ def parse(
multi_lines.extendleft(line_stripped.split(";"))
line = multi_lines.pop()
line_stripped = line

# Test for scope end
if file_ast.END_SCOPE_REGEX is not None:
# treat intermediate folding lines in scopes they exist
if (
file_ast.END_SCOPE_REGEX == FRegex.END_IF
and FRegex.ELSE_IF.match(line_no_comment) is not None
) or (
file_ast.END_SCOPE_REGEX == FRegex.END_SELECT
and FRegex.SELECT_CASE.match(line_no_comment) is not None
):
file_ast.scope_list[-1].mlines.append(line_no)

match = FRegex.END_WORD.match(line_no_comment)
# Handle end statement
if self.parse_end_scope_word(line_no_comment, line_no, file_ast, match):
Expand Down
1 change: 1 addition & 0 deletions fortls/parsers/internal/scope.py
Expand Up @@ -37,6 +37,7 @@ def __init__(
keywords = []
self.file_ast: FortranAST = file_ast
self.sline: int = line_number
self.mlines: list = []
self.eline: int = line_number
self.name: str = name
self.children: list[T[Scope]] = []
Expand Down
4 changes: 4 additions & 0 deletions fortls/regex_patterns.py
Expand Up @@ -146,6 +146,10 @@ class FortranRegularExpressions:
r" |MODULE|PROGRAM|SUBROUTINE|FUNCTION|PROCEDURE|TYPE|DO|IF|SELECT)?",
I,
)

ELSE_IF: Pattern = compile(r"(^|.*\s)(ELSE$|ELSE(\s)|ELSEIF(\s*\())", I)
SELECT_CASE: Pattern = compile(r"((^|\s*\s)(CASE)(\s*\())", I)

# Object regex patterns
CLASS_VAR: Pattern = compile(r"(TYPE|CLASS)[ ]*\(", I)
DEF_KIND: Pattern = compile(r"(\w*)[ ]*\((?:KIND|LEN)?[ =]*(\w*)", I)
Expand Down
1 change: 1 addition & 0 deletions test/test_server.py
Expand Up @@ -181,6 +181,7 @@ def check_return(result_array):
["test_free", 2, 0],
["test_gen_type", 5, 1],
["test_generic", 2, 0],
["test_if_folding", 2, 0],
["test_inherit", 2, 0],
["test_int", 2, 0],
["test_mod", 2, 0],
Expand Down
36 changes: 36 additions & 0 deletions test/test_server_folding.py
@@ -0,0 +1,36 @@
from setup_tests import Path, run_request, test_dir, write_rpc_request


def folding_req(file_path: Path) -> str:
return write_rpc_request(
1,
"textDocument/foldingRange",
{"textDocument": {"uri": str(file_path)}},
)


def validate_folding(results: list, ref: list):
assert len(results) == len(ref)
for i in range(0, len(results)):
assert results[i] == ref[i]


def test_if_folding():
"""Test the ranges for several blocks are correct"""
string = write_rpc_request(1, "initialize", {"rootPath": str(test_dir)})
file_path = test_dir / "subdir" / "test_if_folding.f90"
string += folding_req(file_path)
errcode, results = run_request(string)
assert errcode == 0
ref = [
{"startLine": 0, "endLine": 31},
{"startLine": 10, "endLine": 16},
{"startLine": 11, "endLine": 15},
{"startLine": 13, "endLine": 14},
{"startLine": 19, "endLine": 23},
{"startLine": 24, "endLine": 27},
{"startLine": 28, "endLine": 29},
{"startLine": 2, "endLine": 5},
{"startLine": 20, "endLine": 22},
]
validate_folding(results[1], ref)
2 changes: 2 additions & 0 deletions test/test_source/f90_config.json
Expand Up @@ -21,6 +21,8 @@
"hover_signature": true,
"hover_language": "FortranFreeForm",

"folding_range": true,

"max_line_length": 80,
"max_comment_line_length": 80,
"disable_diagnostics": true,
Expand Down
1 change: 1 addition & 0 deletions test/test_source/pp/.pp_conf.json
Expand Up @@ -3,6 +3,7 @@
"use_signature_help": true,
"variable_hover": true,
"hover_signature": true,
"folding_range": true,
"enable_code_actions": true,
"pp_suffixes": [".h", ".F90"],
"incl_suffixes": [".h"],
Expand Down
33 changes: 33 additions & 0 deletions test/test_source/subdir/test_if_folding.f90
@@ -0,0 +1,33 @@
program test_if_folding

! adding some comment lines
! to check is comment folding
! works
! as expected

implicit none
integer :: i, j

if (i > 0 .and. j > 0) then
if (i > j) then
j = j + 1
if (mod(j,100) == 0) then
print*, "j = ", j
end if
end if
end if

if (j == i) then
! testing some
! comment lines
! right here
print*, "well done"
else if(.true.) then
print*, "missed something..."
print*, "something more"
! random comment here
else
print*, "something else"
end if

end program test_if_folding