Skip to content

provide folding range #459

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

Open
wants to merge 20 commits into
base: master
Choose a base branch
from
3 changes: 3 additions & 0 deletions .github/workflows/docs_preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ on:
concurrency: preview-${{github.ref}}

jobs:
job-name:
permissions:
contents: write
deploy-preview:
runs-on: ubuntu-latest
steps:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
fail-fast: false
runs-on: ${{ matrix.os }}

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ An example for a Configuration file is given below
"lowercase_intrinsics": true,
"hover_signature": true,
"use_signature_help": true,
"folding_range": true,
"excl_paths": ["tests/**", "tools/**"],
"excl_suffixes": ["_skip.f90"],
"include_dirs": ["include/**"],
Expand Down
2 changes: 2 additions & 0 deletions docs/options.rst
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,12 @@
"title": "Hover Language",
"type": "string"
},
"folding_range": {
"default": false,
"description": "Fold editor based on language keywords",
"title": "Folding Range",
"type": "boolean"
},
"max_line_length": {
"default": -1,
"description": "Maximum line length (default: -1)",
Expand Down
7 changes: 7 additions & 0 deletions fortls/interface.py
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@
"textDocument/didClose": self.serve_onClose,
"textDocument/didChange": self.serve_onChange,
"textDocument/codeAction": self.serve_codeActions,
"textDocument/foldingRange": self.serve_folding_range,
"initialized": noop,
"workspace/didChangeWatchedFiles": noop,
"workspace/didChangeConfiguration": noop,
Expand Down Expand Up @@ -226,6 +227,7 @@
"renameProvider": True,
"workspaceSymbolProvider": True,
"textDocumentSync": self.sync_type,
"foldingRangeProvider": True,
}
if self.use_signature_help:
server_capabilities["signatureHelpProvider"] = {
Expand Down Expand Up @@ -1250,6 +1252,56 @@
action["diagnostics"] = new_diags
return action_list

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

Check warning on line 1263 in fortls/langserver.py

View check run for this annotation

Codecov / codecov/patch

fortls/langserver.py#L1263

Added line #L1263 was not covered by tests
if file_obj.ast is None:
return None

Check warning on line 1265 in fortls/langserver.py

View check run for this annotation

Codecov / codecov/patch

fortls/langserver.py#L1265

Added line #L1265 was not covered by tests
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

Check warning on line 1274 in fortls/langserver.py

View check run for this annotation

Codecov / codecov/patch

fortls/langserver.py#L1274

Added line #L1274 was not covered by tests
# Construct folding_rage list:
folding_ranges = []
# first treat scope objects ...
for scope in file_obj.ast.scope_list:
n_mlines = len(scope.mlines)
# ...with intermediate folding lines (if, select case) ...
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)
# ...and finally 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 send_diagnostics(self, uri: str):
diag_results, diag_exp = self.get_diagnostics(uri)
if diag_results is not None:
Expand Down Expand Up @@ -1621,6 +1673,9 @@
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
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,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
50 changes: 50 additions & 0 deletions fortls/parsers/internal/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -1305,6 +1305,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 @@ -1335,6 +1349,10 @@ def parse(
# Need to keep the line number for registering start of Scopes
line_no_end += len(post_lines)
line = "".join([line] + post_lines)
# Add multilines to folding blocks
if line_no != line_no_end:
file_ast.folding_start.append(line_no)
file_ast.folding_end.append(line_no_end)
line, line_label = strip_line_label(line)
line_stripped = strip_strings(line, maintain_len=True)
# Find trailing comments
Expand All @@ -1353,6 +1371,22 @@ def parse(
line_no_comment = line
# Test for scope end
if file_ast.end_scope_regex is not None:
# treat intermediate folding lines in scopes if they exist
if (
file_ast.end_scope_regex == FRegex.END_IF
and FRegex.ELSE_IF.match(line_no_comment) is not None
):
self.update_scope_mlist(file_ast, "#IF", line_no)
elif (
file_ast.end_scope_regex == FRegex.END_SELECT
and (
FRegex.SELECT_CASE.match(line_no_comment)
or FRegex.CASE_DEFAULT.match(line_no_comment)
)
is not None
):
self.update_scope_mlist(file_ast, "#SELECT", 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 Expand Up @@ -1488,6 +1522,8 @@ def parse(
keywords=keywords,
)
file_ast.add_scope(new_sub, FRegex.END_SUB)
if line_no != line_no_end:
file_ast.scope_list[-1].mlines.append(line_no_end)
log.debug("%s !!! SUBROUTINE - Ln:%d", line, line_no)

elif obj_type == "fun":
Expand Down Expand Up @@ -1684,6 +1720,20 @@ def parse(
log.debug("%s: %s", error["range"], error["message"])
return file_ast

def update_scope_mlist(
self, file_ast: FortranAST, scope_name_prefix: str, line_no: int
):
"""Find the last unclosed scope (eline == sline) containing the
scope_name_prefix and add update its nb of intermediate lines (mlines)"""

i = 1
while True:
scope = file_ast.scope_list[-i]
if (scope_name_prefix in scope.name) and (scope.eline == scope.sline):
scope.mlines.append(line_no)
return
i += 1

def parse_imp_dim(self, line: str):
"""Parse the implicit dimension of an array e.g.
var(3,4), var_name(size(val,1)*10)
Expand Down
1 change: 1 addition & 0 deletions fortls/parsers/internal/scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ def __init__(
self.file_ast: FortranAST = file_ast
self.sline: int = line_number
self.eline: int = line_number
self.mlines: list = []
self.name: str = name
self.children: list[T[Scope]] = []
self.members: list = []
Expand Down
3 changes: 3 additions & 0 deletions fortls/regex_patterns.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,9 @@ 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)
CASE_DEFAULT: Pattern = compile(r"[ ]*CASE[ ]+DEFAULT", I)
# Object regex patterns
CLASS_VAR: Pattern = compile(r"(TYPE|CLASS)[ ]*\(", I)
DEF_KIND: Pattern = compile(r"(\w*)[ ]*\((?:KIND|LEN)?[ =]*(\w*)", I)
Expand Down
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env python

"""Builds the fortls Language Server
"""
"""Builds the fortls Language Server"""

import setuptools

setuptools.setup()
1 change: 1 addition & 0 deletions test/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ def check_return(result_array):
["test_str2", 13, 5],
["test_sub", 6, 8],
["test_vis_mod", 2, 0],
["to_test", 13, 2],
)
assert len(result_array) == len(objs)
for i, obj in enumerate(objs):
Expand Down
76 changes: 76 additions & 0 deletions test/test_server_folding.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
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_folding_if():
"""Test the ranges for several blocks are correct"""
string = write_rpc_request(1, "initialize", {"rootPath": str(test_dir)})
file_path = test_dir / "subdir" / "test_folding_if.f90"
string += folding_req(file_path)
errcode, results = run_request(string)
assert errcode == 0
ref = [
{"startLine": 0, "endLine": 43},
{"startLine": 11, "endLine": 21},
{"startLine": 22, "endLine": 23},
{"startLine": 12, "endLine": 20},
{"startLine": 14, "endLine": 15},
{"startLine": 16, "endLine": 17},
{"startLine": 18, "endLine": 19},
{"startLine": 31, "endLine": 35},
{"startLine": 36, "endLine": 39},
{"startLine": 40, "endLine": 41},
{"startLine": 2, "endLine": 6},
{"startLine": 26, "endLine": 28},
{"startLine": 32, "endLine": 34},
]
validate_folding(results[1], ref)


def test_folding_select_case():
"""Test the ranges for several blocks are correct"""
string = write_rpc_request(1, "initialize", {"rootPath": str(test_dir)})
file_path = test_dir / "subdir" / "test_folding_select_case.f90"
string += folding_req(file_path)
errcode, results = run_request(string)
assert errcode == 0
ref = [
{"startLine": 0, "endLine": 14},
{"startLine": 4, "endLine": 4},
{"startLine": 5, "endLine": 6},
{"startLine": 7, "endLine": 8},
{"startLine": 9, "endLine": 10},
{"startLine": 11, "endLine": 12},
]
validate_folding(results[1], ref)


def test_folding_subroutine():
"""Test the ranges for several blocks are correct"""
string = write_rpc_request(1, "initialize", {"rootPath": str(test_dir)})
file_path = test_dir / "subdir" / "test_folding_subroutine.f90"
string += folding_req(file_path)
errcode, results = run_request(string)
assert errcode == 0
print(results[1])
ref = [
{"startLine": 1, "endLine": 4},
{"startLine": 5, "endLine": 17},
{"startLine": 1, "endLine": 5},
{"startLine": 8, "endLine": 11},
{"startLine": 15, "endLine": 16},
]
validate_folding(results[1], ref)
1 change: 1 addition & 0 deletions test/test_source/f90_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"variable_hover": true,
"hover_signature": true,
"hover_language": "FortranFreeForm",
"folding_range": true,

"max_line_length": 80,
"max_comment_line_length": 80,
Expand Down
1 change: 1 addition & 0 deletions test/test_source/pp/.pp_conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"variable_hover": true,
"hover_signature": true,
"enable_code_actions": true,
"folding_range": true,
"pp_suffixes": [".h", ".F90"],
"incl_suffixes": [".h"],
"include_dirs": ["include"],
Expand Down
Loading