From 2d5ff5ddf23315483febe332f6de7a736e28830a Mon Sep 17 00:00:00 2001 From: gnikit Date: Sun, 30 Oct 2022 17:47:35 +0000 Subject: [PATCH 01/14] feat: Change hover to use MarkupContent MarkedString[] has been deprecated so we switch to full Markdown syntax for hover messages Fixes #45 --- fortls/intrinsics.py | 2 +- fortls/langserver.py | 51 ++++----- fortls/objects.py | 153 ++++++++++++++------------ fortls/parse_fortran.py | 10 +- test/test_preproc.py | 22 ++-- test/test_server_hover.py | 219 +++++++++++++++++++++++--------------- 6 files changed, 261 insertions(+), 196 deletions(-) diff --git a/fortls/intrinsics.py b/fortls/intrinsics.py index b29a0397..45525ec2 100644 --- a/fortls/intrinsics.py +++ b/fortls/intrinsics.py @@ -71,7 +71,7 @@ def get_signature(self): return call_sig, self.doc_str, arg_sigs def get_hover(self, long=False): - return self.doc_str, False + return self.doc_str, None, False def is_callable(self): if self.type == 2: diff --git a/fortls/langserver.py b/fortls/langserver.py index 7c120089..6d4d0c87 100644 --- a/fortls/langserver.py +++ b/fortls/langserver.py @@ -490,9 +490,10 @@ def build_comp( comp_obj["detail"] = candidate.get_desc() if call_sig is not None: comp_obj["detail"] += " " + call_sig - doc_str, _ = candidate.get_hover() - if doc_str is not None: - comp_obj["documentation"] = doc_str + # TODO: doc_str should probably be appended, see LSP standard + hover_msg, doc_str, _ = candidate.get_hover() + if hover_msg is not None: + comp_obj["documentation"] = hover_msg return comp_obj # Get parameters from request @@ -1035,11 +1036,13 @@ def serve_definition(self, request: dict): return None def serve_hover(self, request: dict): - def create_hover(string: str, highlight: bool): - if highlight: - return {"language": self.hover_language, "value": string} - else: - return string + def create_hover(string: str, docs: str | None, fortran: bool): + msg = string + if fortran: + msg = f"```{self.hover_language}\n{string}\n```" + if docs: # if docs is not None or "" + msg += f"\n-----\n{docs}" + return msg def create_signature_hover(): sig_request = request.copy() @@ -1057,7 +1060,8 @@ def create_signature_hover(): f"{arg_doc[:doc_split]} :: " f"{arg_info['label']}{arg_doc[doc_split:]}" ) - return create_hover(arg_string, True) + # TODO: check if correct. I think it's not + return create_hover(arg_string, None, True) except: pass @@ -1066,7 +1070,7 @@ def create_signature_hover(): uri: str = params["textDocument"]["uri"] def_line: int = params["position"]["line"] def_char: int = params["position"]["character"] - path = path_from_uri(uri) + path: str = path_from_uri(uri) file_obj = self.workspace.get(path) if file_obj is None: return None @@ -1075,41 +1079,40 @@ def create_signature_hover(): if var_obj is None: return None # Construct hover information - var_type = var_obj.get_type() + var_type: int = var_obj.get_type() hover_array = [] if var_type in (SUBROUTINE_TYPE_ID, FUNCTION_TYPE_ID): - hover_str, highlight = var_obj.get_hover(long=True) - hover_array.append(create_hover(hover_str, highlight)) + hover_str, docs, highlight = var_obj.get_hover(long=True) + hover_array.append(create_hover(hover_str, docs, highlight)) elif var_type == INTERFACE_TYPE_ID: for member in var_obj.mems: - hover_str, highlight = member.get_hover(long=True) + hover_str, docs, highlight = member.get_hover(long=True) if hover_str is not None: - hover_array.append(create_hover(hover_str, highlight)) + hover_array.append(create_hover(hover_str, docs, highlight)) elif var_type == VAR_TYPE_ID: # Unless we have a Fortran literal include the desc in the hover msg # See get_definition for an explanation about this default name if not var_obj.desc.startswith(FORTRAN_LITERAL): - hover_str, highlight = var_obj.get_hover() - hover_array.append(create_hover(hover_str, highlight)) + hover_str, docs, highlight = var_obj.get_hover() + hover_array.append(create_hover(hover_str, docs, highlight)) # Hover for Literal variables elif var_obj.desc.endswith("REAL"): - hover_array.append(create_hover("REAL", True)) + hover_array.append(create_hover("REAL", None, True)) elif var_obj.desc.endswith("INTEGER"): - hover_array.append(create_hover("INTEGER", True)) + hover_array.append(create_hover("INTEGER", None, True)) elif var_obj.desc.endswith("LOGICAL"): - hover_array.append(create_hover("LOGICAL", True)) + hover_array.append(create_hover("LOGICAL", None, True)) elif var_obj.desc.endswith("STRING"): hover_str = f"CHARACTER(LEN={len(var_obj.name)-2})" - hover_array.append(create_hover(hover_str, True)) + hover_array.append(create_hover(hover_str, None, True)) # Include the signature if one is present e.g. if in an argument list if self.hover_signature: - hover_str = create_signature_hover() + hover_str: str | None = create_signature_hover() if hover_str is not None: hover_array.append(hover_str) - # if len(hover_array) > 0: - return {"contents": hover_array} + return {"contents": {"kind": "markdown", "value": "\n".join(hover_array)}} return None def serve_implementation(self, request: dict): diff --git a/fortls/objects.py b/fortls/objects.py index 215ec966..2ccb18fd 100644 --- a/fortls/objects.py +++ b/fortls/objects.py @@ -409,8 +409,8 @@ def get_placeholders(arg_list: list[str]): def get_documentation(self): return self.doc_str - def get_hover(self, long=False, include_doc=True, drop_arg=-1): - return None, False + def get_hover(self, long=False, drop_arg=-1) -> tuple[str | None, str | None, bool]: + return None, None, False def get_signature(self, drop_arg=-1): return None, None, None @@ -920,30 +920,52 @@ def get_snippet(self, name_replace=None, drop_arg=-1): def get_desc(self): return "SUBROUTINE" - def get_hover(self, long=False, include_doc=True, drop_arg=-1): + def get_hover(self, long=False, drop_arg=-1): sub_sig, _ = self.get_snippet(drop_arg=drop_arg) keyword_list = get_keywords(self.keywords) keyword_list.append(f"{self.get_desc()} ") hover_array = [" ".join(keyword_list) + sub_sig] - hover_array = self.get_docs_full(hover_array, long, include_doc, drop_arg) - return "\n ".join(hover_array), long + hover_array, docs = self.get_docs_full(hover_array, long, drop_arg) + return "\n ".join(hover_array), " \n".join(docs), long def get_docs_full( - self, hover_array: list[str], long=False, include_doc=True, drop_arg=-1 - ): + self, hover_array: list[str], long=False, drop_arg=-1 + ) -> tuple[list[str], list[str]]: + """Construct the full documentation with the code signature and the + documentation string + the documentation of any arguments. + + Parameters + ---------- + hover_array : list[str] + The list of strings to append the documentation to. + long : bool, optional + Whether or not to fetch the docs of the arguments, by default False + drop_arg : int, optional + Whether or not to drop certain arguments from the results, by default -1 + + Returns + ------- + tuple[list[str], list[str]] + Tuple containing the Fortran signature that should be in code blocks + and the documentation string that should be in normal Markdown. + """ + doc_strs: list[str] = [] doc_str = self.get_documentation() - if include_doc and doc_str is not None: - hover_array[0] += "\n" + doc_str + if doc_str is not None: + doc_strs.append(doc_str) if long: + has_args = True for i, arg_obj in enumerate(self.arg_objs): if arg_obj is None or i == drop_arg: continue - arg_doc, _ = arg_obj.get_hover(include_doc=False) - hover_array.append(f"{arg_doc} :: {arg_obj.name}") - doc_str = arg_obj.get_documentation() - if include_doc and (doc_str is not None): - hover_array += doc_str.splitlines() - return hover_array + arg, doc_str, _ = arg_obj.get_hover() + hover_array.append(arg) + if doc_str: # If doc_str is not None or "" + if has_args: + doc_strs.append("\n**Parameters:** ") + has_args = False + doc_strs.append(f"`{arg_obj.name}` {doc_str}") + return hover_array, doc_strs def get_signature(self, drop_arg=-1): arg_sigs = [] @@ -964,6 +986,7 @@ def get_signature(self, drop_arg=-1): call_sig, _ = self.get_snippet() return call_sig, self.get_documentation(), arg_sigs + # TODO: fix this def get_interface_array( self, keywords: list[str], signature: str, change_arg=-1, change_strings=None ): @@ -971,7 +994,7 @@ def get_interface_array( for i, arg_obj in enumerate(self.arg_objs): if arg_obj is None: return None - arg_doc, _ = arg_obj.get_hover(include_doc=False) + arg_doc, docs, _ = arg_obj.get_hover() if i == change_arg: i0 = arg_doc.lower().find(change_strings[0].lower()) if i0 >= 0: @@ -1087,8 +1110,8 @@ def is_callable(self): return False def get_hover( - self, long: bool = False, include_doc: bool = True, drop_arg: int = -1 - ) -> tuple[str, bool]: + self, long: bool = False, drop_arg: int = -1 + ) -> tuple[str, str, bool]: """Construct the hover message for a FUNCTION. Two forms are produced here the `long` i.e. the normal for hover requests @@ -1107,8 +1130,6 @@ def get_hover( ---------- 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 @@ -1124,17 +1145,21 @@ def get_hover( keyword_list.append("FUNCTION") hover_array = [f"{' '.join(keyword_list)} {fun_sig}"] - hover_array = self.get_docs_full(hover_array, long, include_doc, drop_arg) + hover_array, docs = self.get_docs_full(hover_array, long, 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}") + # Parse the documentation from the result variable + arg_doc, doc_str, _ = self.result_obj.get_hover() + if doc_str is not None: + docs.append(f"\n**Return:** \n`{self.result_obj.name}`{doc_str}") + hover_array.append(arg_doc) # 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 + return "\n ".join(hover_array), " \n".join(docs), long + # TODO: fix this 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})" @@ -1149,7 +1174,7 @@ def get_interface(self, name_replace=None, change_arg=-1, change_strings=None): keyword_list, fun_sig, change_arg, change_strings ) if self.result_obj is not None: - arg_doc, _ = self.result_obj.get_hover(include_doc=False) + arg_doc, docs, _ = self.result_obj.get_hover() interface_array.append(f"{arg_doc} :: {self.result_obj.name}") name = self.name if name_replace is not None: @@ -1656,18 +1681,17 @@ def get_snippet(self, name_replace=None, drop_arg=-1): # Normal variable return None, None - def get_hover(self, long=False, include_doc=True, drop_arg=-1): + def get_hover(self, long=False, drop_arg=-1) -> tuple[str, str, bool]: doc_str = self.get_documentation() # In associated blocks we need to fetch the desc and keywords of the # linked object hover_str = ", ".join([self.get_desc()] + self.get_keywords()) - # TODO: at this stage we can mae this lowercase - # Add parameter value in the output + # If this is not a preprocessor variable, we can append the variable name + if not hover_str.startswith("#"): + hover_str += f" :: {self.name}" if self.is_parameter() and self.param_val: - hover_str += f" :: {self.name} = {self.param_val}" - if include_doc and (doc_str is not None): - hover_str += "\n {}".format("\n ".join(doc_str.splitlines())) - return hover_str, True + hover_str += f" = {self.param_val}" + return hover_str, doc_str, True def get_keywords(self): # TODO: if local keywords are set they should take precedence over link_obj @@ -1803,45 +1827,34 @@ def get_documentation(self): return self.link_obj.get_documentation() return self.doc_str - def get_hover(self, long=False, include_doc=True, drop_arg=-1): - doc_str = self.get_documentation() - if long: - if self.link_obj is None: - sub_sig, _ = self.get_snippet() - hover_str = f"{self.get_desc()} {sub_sig}" - if include_doc and (doc_str is not None): - hover_str += f"\n{doc_str}" - else: - link_hover, _ = self.link_obj.get_hover( - long=True, include_doc=include_doc, drop_arg=self.drop_arg - ) - hover_split = link_hover.splitlines() - call_sig = hover_split[0] - paren_start = call_sig.rfind("(") - link_name_len = len(self.link_obj.name) - call_sig = ( - call_sig[: paren_start - link_name_len] - + self.name - + call_sig[paren_start:] - ) - hover_split = hover_split[1:] - if include_doc and (self.doc_str is not None): - # Replace linked docs with current object's docs - if (len(hover_split) > 0) and (hover_split[0].count("!!") > 0): - for (i, hover_line) in enumerate(hover_split): - if hover_line.count("!!") == 0: - hover_split = hover_split[i:] - break - else: # All lines are docs - hover_split = [] - hover_split = [self.doc_str] + hover_split - hover_str = "\n".join([call_sig] + hover_split) - return hover_str, True - else: + def get_hover(self, long=False, drop_arg=-1) -> tuple[str, str, bool]: + docs = self.get_documentation() + if not long: hover_str = ", ".join([self.desc] + get_keywords(self.keywords)) - if include_doc and (doc_str is not None): - hover_str += f"\n{doc_str}" - return hover_str, True + return hover_str, docs, True + # Long hover message + if self.link_obj is None: + sub_sig, _ = self.get_snippet() + hover_str = f"{self.get_desc()} {sub_sig}" + else: + link_msg, link_docs, _ = self.link_obj.get_hover( + long=True, drop_arg=self.drop_arg + ) + # Replace the name of the linked object with the name of this object + hover_str = link_msg.replace(self.link_obj.name, self.name, 1) + if isinstance(link_docs, str): + # Get just the docstring of the link, if any, no args + link_doc_top = self.link_obj.get_documentation() + # Replace the linked objects topmost documentation with the + # documentation of the procedure pointer if one is present + if link_doc_top is not None: + docs = link_docs.replace(link_doc_top, docs, 1) + # If no top docstring is present at the linked object but there + # are docstrings for the arguments, add them to the end of the + # documentation for this object + elif link_docs: + docs += " \n" + link_docs + return hover_str, docs, True def get_signature(self, drop_arg=-1): if self.link_obj is not None: diff --git a/fortls/parse_fortran.py b/fortls/parse_fortran.py index 8f3ba2c4..f917ecc5 100644 --- a/fortls/parse_fortran.py +++ b/fortls/parse_fortran.py @@ -13,7 +13,7 @@ except ImportError: from typing_extensions import Literal -from re import Pattern +from re import Pattern, Match from fortls.constants import ( DO_TYPE_ID, @@ -1858,8 +1858,8 @@ def parse_docs(self, line: str, ln: int, file_ast: FortranAST, docs: list[str]): def format(docs: list[str]) -> str: if len(docs) == 1: - return f"!! {docs[0]}" - return "!! " + "\n!! ".join(docs) + return f"{docs[0]}" + return "\n".join(docs) def add_line_comment(file_ast: FortranAST, docs: list[str]): # Handle dangling comments from previous line @@ -1891,7 +1891,7 @@ def add_line_comment(file_ast: FortranAST, docs: list[str]): return ln def get_docstring( - self, ln: int, line: str, match: Pattern, docs: list[str] + self, ln: int, line: str, match: Match[str], docs: list[str] ) -> tuple[int, list[str], bool]: """Extract entire documentation strings from the current file position @@ -1901,7 +1901,7 @@ def get_docstring( Line number line : str Document line, not necessarily produced by `get_line()` - match : Pattern + match : Match[str] Regular expression DOC match docs : list[str] Docstrings that are pending processing e.g. single line docstrings diff --git a/test/test_preproc.py b/test/test_preproc.py index 35dce5af..b149c648 100644 --- a/test/test_preproc.py +++ b/test/test_preproc.py @@ -17,7 +17,7 @@ def hover_req(file_path: str, ln: int, col: int) -> str: def check_return(result_array, checks): assert len(result_array) == len(checks) for (i, check) in enumerate(checks): - assert result_array[i]["contents"][0]["value"] == check + assert result_array[i]["contents"]["value"] == check root_dir = test_dir / "pp" string = write_rpc_request(1, "initialize", {"rootPath": str(root_dir)}) @@ -36,13 +36,19 @@ def check_return(result_array, checks): # Reference solution ref_results = ( - "#define PCType character*(80)", - "#define PETSC_ERR_INT_OVERFLOW 84", - "#define varVar 55", - "#define ewrite if (priority <= 3) write((priority), format)", - "#define ewrite2 if (priority <= 3) write((priority), format)", - "#define SUCCESS .true.", - "REAL, CONTIGUOUS, POINTER, DIMENSION(:)", + "```fortran90\n#define PCType character*(80)\n```", + "```fortran90\n#define PETSC_ERR_INT_OVERFLOW 84\n```", + "```fortran90\n#define varVar 55\n```", + ( + "```fortran90\n#define ewrite if (priority <= 3) write((priority)," + " format)\n```" + ), + ( + "```fortran90\n#define ewrite2 if (priority <= 3) write((priority)," + " format)\n```" + ), + "```fortran90\n#define SUCCESS .true.\n```", + "```fortran90\nREAL, CONTIGUOUS, POINTER, DIMENSION(:) :: var1\n```", ) assert len(ref_results) == len(results) - 1 check_return(results[1:], ref_results) diff --git a/test/test_server_hover.py b/test/test_server_hover.py index dd2b355e..4bbd4fe1 100644 --- a/test/test_server_hover.py +++ b/test/test_server_hover.py @@ -15,7 +15,7 @@ def hover_req(file_path: str, ln: int, col: int) -> str: def validate_hover(result_array: list, checks: list): assert len(result_array) - 1 == len(checks) for (i, check) in enumerate(checks): - assert result_array[i + 1]["contents"][0]["value"] == check + assert result_array[i + 1]["contents"]["value"] == check def test_hover_abstract_int_procedure(): @@ -26,9 +26,11 @@ def test_hover_abstract_int_procedure(): errcode, results = run_request(string, fortls_args=["--sort_keywords", "-n1"]) assert errcode == 0 ref_results = [ - """SUBROUTINE test(a, b) + """```fortran90 +SUBROUTINE test(a, b) INTEGER(4), DIMENSION(3,6), INTENT(IN) :: a - REAL(8), DIMENSION(4), INTENT(OUT) :: b""" + REAL(8), DIMENSION(4), INTENT(OUT) :: b +```""" ] validate_hover(results, ref_results) @@ -40,7 +42,7 @@ def test_hover_parameter_multiline(): string += hover_req(file_path, 2, 28) errcode, results = run_request(string, fortls_args=["--sort_keywords"]) assert errcode == 0 - ref_results = ["INTEGER, PARAMETER :: var = 1000"] + ref_results = ["```fortran90\nINTEGER, PARAMETER :: var = 1000\n```"] validate_hover(results, ref_results) @@ -51,7 +53,7 @@ def test_hover_literal_num(): string += hover_req(file_path, 3, 28) errcode, results = run_request(string, fortls_args=["--sort_keywords"]) assert errcode == 0 - ref_results = ["INTEGER"] + ref_results = ["```fortran90\nINTEGER\n```"] validate_hover(results, ref_results) @@ -62,7 +64,7 @@ def test_hover_parameter(): string += hover_req(file_path, 4, 28) errcode, results = run_request(string, fortls_args=["--sort_keywords"]) assert errcode == 0 - ref_results = ["INTEGER, PARAMETER :: var2 = 23"] + ref_results = ["```fortran90\nINTEGER, PARAMETER :: var2 = 23\n```"] validate_hover(results, ref_results) @@ -73,7 +75,7 @@ def test_hover_parameter_nested(): string += hover_req(file_path, 4, 41) errcode, results = run_request(string, fortls_args=["--sort_keywords"]) assert errcode == 0 - ref_results = ["INTEGER, PARAMETER :: var3 = var*var2"] + ref_results = ["```fortran90\nINTEGER, PARAMETER :: var3 = var*var2\n```"] validate_hover(results, ref_results) @@ -84,7 +86,7 @@ def test_hover_parameter_multiline_missing_type(): string += hover_req(file_path, 6, 28) errcode, results = run_request(string, fortls_args=["--sort_keywords"]) assert errcode == 0 - ref_results = ["INTEGER, PARAMETER :: var4 = 123"] + ref_results = ["```fortran90\nINTEGER, PARAMETER :: var4 = 123\n```"] validate_hover(results, ref_results) @@ -95,7 +97,7 @@ def test_hover_literal_real(): string += hover_req(file_path, 7, 47) errcode, results = run_request(string, fortls_args=["--sort_keywords"]) assert errcode == 0 - ref_results = ["REAL"] + ref_results = ["```fortran90\nREAL\n```"] validate_hover(results, ref_results) @@ -106,7 +108,7 @@ def test_hover_parameter_double(): string += hover_req(file_path, 7, 38) errcode, results = run_request(string, fortls_args=["--sort_keywords"]) assert errcode == 0 - ref_results = ["DOUBLE PRECISION, PARAMETER :: somevar = 23.12"] + ref_results = ["```fortran90\nDOUBLE PRECISION, PARAMETER :: somevar = 23.12\n```"] validate_hover(results, ref_results) @@ -117,7 +119,7 @@ def test_hover_parameter_double_sf(): string += hover_req(file_path, 7, 55) errcode, results = run_request(string, fortls_args=["--sort_keywords"]) assert errcode == 0 - ref_results = ["DOUBLE PRECISION, PARAMETER :: some = 1e-19"] + ref_results = ["```fortran90\nDOUBLE PRECISION, PARAMETER :: some = 1e-19\n```"] validate_hover(results, ref_results) @@ -128,7 +130,9 @@ def test_hover_parameter_bool(): string += hover_req(file_path, 8, 38) errcode, results = run_request(string, fortls_args=["--sort_keywords"]) assert errcode == 0 - ref_results = ["LOGICAL(kind=8), PARAMETER :: long_bool = .true."] + ref_results = [ + "```fortran90\nLOGICAL(kind=8), PARAMETER :: long_bool = .true.\n```" + ] validate_hover(results, ref_results) @@ -139,7 +143,7 @@ def test_hover_literal_bool(): string += hover_req(file_path, 8, 50) errcode, results = run_request(string, fortls_args=["--sort_keywords"]) assert errcode == 0 - ref_results = ["LOGICAL"] + ref_results = ["```fortran90\nLOGICAL\n```"] validate_hover(results, ref_results) @@ -150,7 +154,7 @@ def test_hover_parameter_str_sq(): string += hover_req(file_path, 9, 37) errcode, results = run_request(string, fortls_args=["--sort_keywords"]) assert errcode == 0 - ref_results = ["CHARACTER(len=5), PARAMETER :: sq_str = '12345'"] + ref_results = ["```fortran90\nCHARACTER(len=5), PARAMETER :: sq_str = '12345'\n```"] validate_hover(results, ref_results) @@ -161,7 +165,7 @@ def test_hover_literal_string_sq(): string += hover_req(file_path, 9, 48) errcode, results = run_request(string, fortls_args=["--sort_keywords"]) assert errcode == 0 - ref_results = ["CHARACTER(LEN=5)"] + ref_results = ["```fortran90\nCHARACTER(LEN=5)\n```"] validate_hover(results, ref_results) @@ -172,7 +176,7 @@ def test_hover_parameter_str_dq(): string += hover_req(file_path, 10, 37) errcode, results = run_request(string, fortls_args=["--sort_keywords"]) assert errcode == 0 - ref_results = ['CHARACTER(len=5), PARAMETER :: dq_str = "12345"'] + ref_results = ['```fortran90\nCHARACTER(len=5), PARAMETER :: dq_str = "12345"\n```'] validate_hover(results, ref_results) @@ -183,7 +187,7 @@ def test_hover_literal_string_dq(): string += hover_req(file_path, 10, 48) errcode, results = run_request(string, fortls_args=["--sort_keywords"]) assert errcode == 0 - ref_results = ["CHARACTER(LEN=5)"] + ref_results = ["```fortran90\nCHARACTER(LEN=5)\n```"] validate_hover(results, ref_results) @@ -194,7 +198,7 @@ def test_hover_pointer_attr(): string += hover_req(file_path, 1, 26) errcode, results = run_request(string, fortls_args=["--sort_keywords"]) assert errcode == 0 - ref_results = ["INTEGER, POINTER"] + ref_results = ["```fortran90\nINTEGER, POINTER :: val1\n```"] validate_hover(results, ref_results) @@ -216,36 +220,56 @@ def test_hover_functions(): assert errcode == 0 ref_results = [ - """FUNCTION fun1(arg) RESULT(fun1) + """```fortran90 +FUNCTION fun1(arg) RESULT(fun1) INTEGER, INTENT(IN) :: arg - INTEGER :: fun1""", - """FUNCTION fun2(arg) RESULT(fun2) + INTEGER :: fun1 +```""", + """```fortran90 +FUNCTION fun2(arg) RESULT(fun2) INTEGER, INTENT(IN) :: arg - INTEGER :: fun2""", - """FUNCTION fun3(arg) RESULT(retval) + INTEGER :: fun2 +```""", + """```fortran90 +FUNCTION fun3(arg) RESULT(retval) INTEGER, INTENT(IN) :: arg - INTEGER :: retval""", - """FUNCTION fun4(arg) RESULT(retval) + INTEGER :: retval +```""", + """```fortran90 +FUNCTION fun4(arg) RESULT(retval) INTEGER, INTENT(IN) :: arg - INTEGER :: retval""", + INTEGER :: retval +```""", # Notice that the order of the modifiers does not match the source code # This is part of the test, ideally they would be identical but previously # any modifiers before the type would be discarded - """PURE ELEMENTAL FUNCTION fun5(arg) RESULT(retval) + """```fortran90 +PURE ELEMENTAL FUNCTION fun5(arg) RESULT(retval) INTEGER, INTENT(IN) :: arg - INTEGER :: retval""", - """FUNCTION fun6(arg) RESULT(retval) + INTEGER :: retval +```""", + """```fortran90 +FUNCTION fun6(arg) RESULT(retval) INTEGER, INTENT(IN) :: arg - INTEGER, DIMENSION(10,10) :: retval""", - """PURE FUNCTION outer_product(x, y) RESULT(outer_product) + INTEGER, DIMENSION(10,10) :: retval +```""", + """```fortran90 +PURE FUNCTION outer_product(x, y) RESULT(outer_product) REAL, DIMENSION(:), INTENT(IN) :: x REAL, DIMENSION(:), INTENT(IN) :: y - REAL, DIMENSION(SIZE(X), SIZE(Y)) :: outer_product""", - """FUNCTION dlamch(cmach) RESULT(dlamch) - CHARACTER :: CMACH""", - """FUNCTION fun7() RESULT(val) - TYPE(c_ptr) :: val""", - """TYPE(c_ptr) FUNCTION c_loc(x) RESULT(c_loc)""", + REAL, DIMENSION(SIZE(X), SIZE(Y)) :: outer_product +```""", + """```fortran90 +FUNCTION dlamch(cmach) RESULT(dlamch) + CHARACTER :: CMACH +```""", + """```fortran90 +FUNCTION fun7() RESULT(val) + TYPE(c_ptr) :: val +```""", + """```fortran90 +TYPE(c_ptr) FUNCTION c_loc(x) RESULT(c_loc) +```""", ] validate_hover(results, ref_results) @@ -258,9 +282,9 @@ def test_hover_spaced_keywords(): errcode, results = run_request(string, fortls_args=["--sort_keywords"]) assert errcode == 0 ref_results = [ - """REAL, DIMENSION(:, :), INTENT(IN)""", - """REAL, DIMENSION( SIZE(ARG1, 1), MAXVAL([SIZE(ARG1, 2), """ - """SIZE(ARG1, 1)]) ), INTENT(OUT)""", + """```fortran90\nREAL, DIMENSION(:, :), INTENT(IN) :: arg1\n```""", + """```fortran90\nREAL, DIMENSION( SIZE(ARG1, 1), MAXVAL([SIZE(ARG1, 2), """ + """SIZE(ARG1, 1)]) ), INTENT(OUT) :: arg2\n```""", ] validate_hover(results, ref_results) @@ -272,10 +296,12 @@ def test_hover_recursive(): errcode, results = run_request(string, fortls_args=["--sort_keywords"]) assert errcode == 0 ref_results = [ - """RECURSIVE SUBROUTINE recursive_assign_descending(node, vector, current_loc) + """```fortran90 +RECURSIVE SUBROUTINE recursive_assign_descending(node, vector, current_loc) TYPE(tree_inode), POINTER, INTENT(IN) :: node INTEGER, DIMENSION(:), INTENT(INOUT) :: vector - INTEGER, INTENT(INOUT) :: current_loc""" + INTEGER, INTENT(INOUT) :: current_loc +```""" ] validate_hover(results, ref_results) @@ -288,14 +314,18 @@ def test_hover_subroutine(): errcode, results = run_request(string, fortls_args=["--sort_keywords"]) assert errcode == 0 ref_results = [ - """FUNCTION point_dist(a, b) RESULT(distance) + """```fortran90 +FUNCTION point_dist(a, b) RESULT(distance) TYPE(point), INTENT(IN) :: a TYPE(point), INTENT(IN) :: b - REAL :: distance""", - """FUNCTION is_point_equal_a(a, b) RESULT(is_point_equal_a) + REAL :: distance +```""", + """```fortran90 +FUNCTION is_point_equal_a(a, b) RESULT(is_point_equal_a) TYPE(point), INTENT(IN) :: a TYPE(point), INTENT(IN) :: b - LOGICAL :: is_point_equal_a""", + LOGICAL :: is_point_equal_a +```""", ] validate_hover(results, ref_results) @@ -304,15 +334,17 @@ def test_hover_interface_as_argument(): string = write_rpc_request(1, "initialize", {"rootPath": str(test_dir)}) file_path = test_dir / "test_diagnostic_int.f90" string += hover_req(file_path, 19, 14) - errcode, results = run_request(string, fortls_args=["--sort_keywords"]) + errcode, results = run_request(string, fortls_args=["--sort_keywords", "-n1"]) assert errcode == 0 ref_results = ( # Could be subject to change - """FUNCTION foo2(f, g, h) RESULT(arg3) - FUNCTION f(x) :: f - FUNCTION g(x) :: g - FUNCTION h(x) :: h - REAL :: arg3""", + """```fortran90 +FUNCTION foo2(f, g, h) RESULT(arg3) + FUNCTION f(x) + FUNCTION g(x) + FUNCTION h(x) + REAL :: arg3 +```""", ) validate_hover(results, ref_results) @@ -325,7 +357,10 @@ def test_hover_block(): # string += hover_req(file_path, 10, 11) # slice of array errcode, results = run_request(string, fortls_args=["--sort_keywords", "-n", "1"]) assert errcode == 0 - ref_results = ["REAL, DIMENSION(5)", "REAL"] + ref_results = [ + "```fortran90\nREAL, DIMENSION(5) :: X\n```", + "```fortran90\nREAL :: Y\n```", + ] validate_hover(results, ref_results) @@ -340,12 +375,16 @@ def test_hover_submodule_procedure(): errcode, results = run_request(string, fortls_args=["-n", "1"]) assert errcode == 0 ref_results = [ - """PURE RECURSIVE FUNCTION foo_sp(x) RESULT(fi) + """```fortran90 +PURE RECURSIVE FUNCTION foo_sp(x) RESULT(fi) REAL(sp), INTENT(IN) :: x - REAL(sp) :: fi""", - """PURE RECURSIVE FUNCTION foo_dp(x) RESULT(fi) + REAL(sp) :: fi +```""", + """```fortran90 +PURE RECURSIVE FUNCTION foo_dp(x) RESULT(fi) REAL(dp), INTENT(IN) :: x - REAL(dp) :: fi""", + REAL(dp) :: fi +```""", ] validate_hover(results, ref_results) @@ -364,14 +403,14 @@ def test_var_type_kinds(): errcode, results = run_request(string, fortls_args=["-n", "1"]) assert errcode == 0 ref_results = [ - "INTEGER(kind=4)", - "INTEGER(kind=4), DIMENSION(3,4)", - "INTEGER*8", - "INTEGER*8, DIMENSION(3,4)", - "INTEGER(8)", - "INTEGER(8), DIMENSION(3,4)", - "REAL(kind=r15)", - "REAL(kind(0.d0))", + "```fortran90\nINTEGER(kind=4) :: a\n```", + "```fortran90\nINTEGER(kind=4), DIMENSION(3,4) :: b\n```", + "```fortran90\nINTEGER*8 :: aa\n```", + "```fortran90\nINTEGER*8, DIMENSION(3,4) :: bb\n```", + "```fortran90\nINTEGER(8) :: aaa\n```", + "```fortran90\nINTEGER(8), DIMENSION(3,4) :: bbb\n```", + "```fortran90\nREAL(kind=r15) :: r\n```", + "```fortran90\nREAL(kind(0.d0)) :: rr\n```", ] validate_hover(results, ref_results) @@ -384,12 +423,16 @@ def test_kind_function_result(): errcode, results = run_request(string, fortls_args=["-n", "1"]) assert errcode == 0 ref_results = [ - """FUNCTION foo(val) RESULT(r) + """```fortran90 +FUNCTION foo(val) RESULT(r) REAL(8), INTENT(IN) :: val - REAL*8 :: r""", - """FUNCTION phi(val) RESULT(r) + REAL*8 :: r +```""", + """```fortran90 +FUNCTION phi(val) RESULT(r) REAL(8), INTENT(IN) :: val - REAL(kind=8) :: r""", + REAL(kind=8) :: r +```""", ] validate_hover(results, ref_results) @@ -406,12 +449,12 @@ def test_var_type_asterisk(): errcode, results = run_request(string, fortls_args=["-n", "1"]) assert errcode == 0 ref_results = [ - "CHARACTER*17", - "CHARACTER*17, DIMENSION(3,4)", - "CHARACTER*17, DIMENSION(9)", - "CHARACTER*(6+3)", - "CHARACTER*10, DIMENSION(3,4)", - "CHARACTER*(LEN(B)), DIMENSION(3,4)", + "```fortran90\nCHARACTER*17 :: A\n```", + "```fortran90\nCHARACTER*17, DIMENSION(3,4) :: B\n```", + "```fortran90\nCHARACTER*17, DIMENSION(9) :: V\n```", + "```fortran90\nCHARACTER*(6+3) :: C\n```", + "```fortran90\nCHARACTER*10, DIMENSION(3,4) :: D\n```", + "```fortran90\nCHARACTER*(LEN(B)), DIMENSION(3,4) :: DD\n```", ] validate_hover(results, ref_results) @@ -431,14 +474,14 @@ def test_var_name_asterisk(): errcode, results = run_request(string, fortls_args=["-n", "1"]) assert errcode == 0 ref_results = [ - "CHARACTER*17", - "CHARACTER*17, DIMENSION(3,4)", - "CHARACTER*17, DIMENSION(9)", - "CHARACTER*(6+3)", - "CHARACTER*(LEN(A))", - "CHARACTER*10, DIMENSION(*)", - "CHARACTER(LEN=200)", - "CHARACTER(KIND=4, LEN=200), DIMENSION(3,4)", + "```fortran90\nCHARACTER*17 :: AA\n```", + "```fortran90\nCHARACTER*17, DIMENSION(3,4) :: BB\n```", + "```fortran90\nCHARACTER*17, DIMENSION(9) :: VV\n```", + "```fortran90\nCHARACTER*(6+3) :: CC\n```", + "```fortran90\nCHARACTER*(LEN(A)) :: AAA\n```", + "```fortran90\nCHARACTER*10, DIMENSION(*) :: INPUT\n```", + "```fortran90\nCHARACTER(LEN=200) :: F\n```", + "```fortran90\nCHARACTER(KIND=4, LEN=200), DIMENSION(3,4) :: FF\n```", # "CHARACTER(KIND=4, LEN=100), DIMENSION(3,4)", ] validate_hover(results, ref_results) @@ -455,10 +498,10 @@ def test_intent(): errcode, results = run_request(string, fortls_args=["-n", "1"]) assert errcode == 0 ref_results = [ - """INTEGER(4), INTENT(IN)""", - """INTEGER, INTENT(OUT)""", - """INTEGER(4), INTENT(INOUT)""", - """INTEGER(4), INTENT(IN OUT)""", - """REAL, OPTIONAL, INTENT(IN)""", + """```fortran90\nINTEGER(4), INTENT(IN) :: arg1\n```""", + """```fortran90\nINTEGER, INTENT(OUT) :: arg2\n```""", + """```fortran90\nINTEGER(4), INTENT(INOUT) :: arg3\n```""", + """```fortran90\nINTEGER(4), INTENT(IN OUT) :: arg4\n```""", + """```fortran90\nREAL, OPTIONAL, INTENT(IN) :: arg5\n```""", ] validate_hover(results, ref_results) From d26753abf04a1def88844edbccbe1fed95195991 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 30 Oct 2022 18:04:34 +0000 Subject: [PATCH 02/14] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- fortls/parse_fortran.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fortls/parse_fortran.py b/fortls/parse_fortran.py index f917ecc5..f45b5016 100644 --- a/fortls/parse_fortran.py +++ b/fortls/parse_fortran.py @@ -13,7 +13,7 @@ except ImportError: from typing_extensions import Literal -from re import Pattern, Match +from re import Match, Pattern from fortls.constants import ( DO_TYPE_ID, From b9c2e9fdc31a36b0669ed83bd0976a0868b9d247 Mon Sep 17 00:00:00 2001 From: gnikit Date: Mon, 31 Oct 2022 14:08:21 +0000 Subject: [PATCH 03/14] feat: added Doxygen parser to Markdown --- fortls/objects.py | 5 +- fortls/parse_fortran.py | 29 +++- test/test_server_documentation.py | 202 +++++++++++++++++++++---- test/test_source/docs/test_doxygen.f90 | 1 + test/test_source/subdir/test_free.f90 | 10 +- 5 files changed, 207 insertions(+), 40 deletions(-) diff --git a/fortls/objects.py b/fortls/objects.py index 2ccb18fd..52c09a52 100644 --- a/fortls/objects.py +++ b/fortls/objects.py @@ -964,7 +964,8 @@ def get_docs_full( if has_args: doc_strs.append("\n**Parameters:** ") has_args = False - doc_strs.append(f"`{arg_obj.name}` {doc_str}") + # stripping prevents multiple \n characters from the parser + doc_strs.append(f"`{arg_obj.name}` {doc_str}".strip()) return hover_array, doc_strs def get_signature(self, drop_arg=-1): @@ -1853,6 +1854,8 @@ def get_hover(self, long=False, drop_arg=-1) -> tuple[str, str, bool]: # are docstrings for the arguments, add them to the end of the # documentation for this object elif link_docs: + if docs is None: + docs = "" docs += " \n" + link_docs return hover_str, docs, True diff --git a/fortls/parse_fortran.py b/fortls/parse_fortran.py index f45b5016..3c070d09 100644 --- a/fortls/parse_fortran.py +++ b/fortls/parse_fortran.py @@ -1857,9 +1857,36 @@ def parse_docs(self, line: str, ln: int, file_ast: FortranAST, docs: list[str]): """ def format(docs: list[str]) -> str: + """Format docstrings and parse for Doxygen tags""" if len(docs) == 1: return f"{docs[0]}" - return "\n".join(docs) + docstr = "" + has_args = True + idx_args = -1 + for (i, line) in enumerate(docs): + if line.startswith("@brief"): + docstr += line.replace("@brief", "", 1).strip() + "\n" + elif line.startswith("@param"): + if has_args: + docstr += "\n**Parameters:** \n" + has_args = False + idx_args = len(docstr) + docstr += re.sub( + r"[@\\]param(?:[\[\(]\s*[\w,]+\s*[\]\)])?\s+(.*?)\s+", + r" \n`\1` - ", + line + " ", + ) + elif line.startswith("@return"): + docstr += "\n**Returns:** \n" + docstr += line.replace("@return", "", 1).strip() + "\n" + else: + docstr += line.strip() + "\n" + # Remove new line characters from 1st @param line + if idx_args > 0: + docstr = docstr[: idx_args - 3] + docstr[idx_args:].replace( + " \n ", "", 1 + ) + return docstr def add_line_comment(file_ast: FortranAST, docs: list[str]): # Handle dangling comments from previous line diff --git a/test/test_server_documentation.py b/test/test_server_documentation.py index 634f6d99..06f377ab 100644 --- a/test/test_server_documentation.py +++ b/test/test_server_documentation.py @@ -1,10 +1,17 @@ from setup_tests import run_request, test_dir, write_rpc_request -def check_return(result_array, checks): +def check_return(result_array, checks, only_docs=False): comm_lines = [] - for (i, hover_line) in enumerate(result_array["contents"][0]["value"].splitlines()): - if hover_line.count("!!") > 0: + found_docs = False + idx = 0 + for (i, hover_line) in enumerate(result_array["contents"]["value"].splitlines()): + if hover_line == "-----": + found_docs = True + if found_docs and only_docs: + comm_lines.append((idx, hover_line)) + idx += 1 + elif not only_docs: comm_lines.append((i, hover_line)) assert len(comm_lines) == len(checks) for i in range(len(checks)): @@ -27,23 +34,35 @@ def test_doxygen(): string = write_rpc_request(1, "initialize", {"rootPath": str(test_dir / "docs")}) file_path = test_dir / "docs" / "test_doxygen.f90" string += hover_request(file_path, 15, 17) - errcode, results = run_request(string) + errcode, results = run_request(string, ["-n1"]) assert errcode == 0 ref = ( - (1, "!! @brief inserts a value into an ordered array"), - (2, "!! "), + (0, "```fortran90"), + (1, "SUBROUTINE insert(list, n, max_size, new_entry)"), + (2, " REAL, DIMENSION(:), INTENT(INOUT) :: list"), + (3, " INTEGER, INTENT(IN) :: n"), + (4, " INTEGER, INTENT(IN) :: max_size"), + (5, " REAL, INTENT(IN) :: new_entry"), + (6, "```"), + (7, "-----"), + (8, "inserts a value into an ordered array"), + (9, ""), ( - 3, - '!! An array "list" consisting of n ascending ordered values. The method' - " insert a", + 10, + ( + 'An array "list" consisting of n ascending ordered values. The method' + " insert a" + ), ), - (4, '!! "new_entry" into the array.'), - (5, "!! hint: use cshift and eo-shift"), - (6, "!! "), - (7, "!! @param[in,out] list a real array, size: max_size"), - (8, "!! @param[in] n current values in the array"), - (9, "!! @param[in] max_size size if the array"), - (10, "!! @param[in] new_entry the value to insert"), + (11, '"new_entry" into the array.'), + (12, "hint: use cshift and eo-shift"), + (13, ""), + (14, ""), + (15, "**Parameters:** "), + (16, "`list` - a real array, size: max_size "), + (17, "`n` - current values in the array "), + (18, "`max_size` - size if the array "), + (19, "`new_entry` - the value to insert "), ) check_return(results[1], ref) @@ -55,12 +74,23 @@ def test_ford(): errcode, results = run_request(string) assert errcode == 0 ref = ( - (1, "!! Feeds your cats and dogs, if enough food is available. If not enough"), - (2, "!! food is available, some of your pets will get angry."), - (4, " !! The number of cats to keep track of."), - (6, " !! The number of dogs to keep track of."), - (8, " !! The amount of pet food (in kilograms) which you have on hand."), - (10, " !! The number of pets angry because they weren't fed."), + (0, "```fortran90"), + (1, "SUBROUTINE feed_pets(cats, dogs, food, angry)"), + (2, " INTEGER, INTENT(IN) :: cats"), + (3, " INTEGER, INTENT(IN) :: dogs"), + (4, " REAL, INTENT(INOUT) :: food"), + (5, " INTEGER, INTENT(OUT) :: angry"), + (6, "```"), + (7, "-----"), + (8, "Feeds your cats and dogs, if enough food is available. If not enough"), + (9, "food is available, some of your pets will get angry."), + (10, " "), + (11, ""), + (12, "**Parameters:** "), + (13, "`cats` The number of cats to keep track of. "), + (14, "`dogs` The number of dogs to keep track of. "), + (15, "`food` The amount of pet food (in kilograms) which you have on hand. "), + (16, "`angry` The number of pets angry because they weren't fed."), ) check_return(results[1], ref) @@ -74,7 +104,17 @@ def test_doc_overwrite_type_bound_procedure_sub(): string += hover_request(file_path, 13, 19) errcode, results = run_request(string) assert errcode == 0 - check_return(results[1], ((1, "!! Doc 1"), (3, " !! Doc 5"))) + check_return( + results[1], + ( + (0, "-----"), + (1, "Doc 1 "), + (2, ""), + (3, "**Parameters:** "), + (4, "`n` Doc 5"), + ), + True, + ) def test_doc_type_bound_procedure_sub_implementation(): @@ -85,7 +125,17 @@ def test_doc_type_bound_procedure_sub_implementation(): string += hover_request(file_path, 13, 31) errcode, results = run_request(string) assert errcode == 0 - check_return(results[1], ((1, "!! Doc 4"), (4, " !! Doc 5"))) + check_return( + results[1], + ( + (0, "-----"), + (1, "Doc 4 "), + (2, ""), + (3, "**Parameters:** "), + (4, "`n` Doc 5"), + ), + True, + ) def test_doc_variable(): @@ -96,7 +146,14 @@ def test_doc_variable(): string += hover_request(file_path, 37, 26) errcode, results = run_request(string) assert errcode == 0 - check_return(results[1], ((1, " !! Doc 5"),)) + check_return( + results[1], + ( + (0, "-----"), + (1, "Doc 5"), + ), + True, + ) def test_doc_overwrite_type_bound_procedure_fun(): @@ -108,7 +165,14 @@ def test_doc_overwrite_type_bound_procedure_fun(): string += hover_request(file_path, 14, 17) errcode, results = run_request(string) assert errcode == 0 - check_return(results[1], ((1, "!! Doc 2"),)) + check_return( + results[1], + ( + (0, "-----"), + (1, "Doc 2"), + ), + True, + ) def test_doc_type_bound_procedure_fun_implementation(): @@ -119,7 +183,14 @@ def test_doc_type_bound_procedure_fun_implementation(): string += hover_request(file_path, 14, 28) errcode, results = run_request(string) assert errcode == 0 - check_return(results[1], ((1, "!! Doc 6"),)) + check_return( + results[1], + ( + (0, "-----"), + (1, "Doc 6"), + ), + True, + ) def test_doc_empty_overwrite_type_bound_procedure_sub(): @@ -128,12 +199,34 @@ def test_doc_empty_overwrite_type_bound_procedure_sub(): # Test we can ignore overriding method docstring and return the original e.g. # procedure :: name => name_imp !< # We want to preserve the argument list docstring + # the self argument in the second request is not included because it is + # missing a doc string string += hover_request(file_path, 21, 18) string += hover_request(file_path, 21, 37) errcode, results = run_request(string) assert errcode == 0 - check_return(results[1], ((1, "!! Doc 7"), (3, " !! Doc 8"))) - check_return(results[2], ((1, "!! Doc 7"), (4, " !! Doc 8"))) + check_return( + results[1], + ( + (0, "-----"), + (1, "Doc 7 "), + (2, ""), + (3, "**Parameters:** "), + (4, "`scale` Doc 8"), + ), + True, + ) + check_return( + results[2], + ( + (0, "-----"), + (1, "Doc 7 "), + (2, ""), + (3, "**Parameters:** "), + (4, "`scale` Doc 8"), + ), + True, + ) def test_doc_empty_overwrite_type_bound_procedure_fun(): @@ -146,8 +239,31 @@ def test_doc_empty_overwrite_type_bound_procedure_fun(): string += hover_request(file_path, 22, 32) errcode, results = run_request(string) assert errcode == 0 - check_return(results[1], ((1, "!! Doc 3"),)) - check_return(results[2], ()) + check_return( + results[1], + ( + (0, "-----"), + (1, "Doc 3 "), + (2, ""), + (3, "**Return:** "), + (4, "`norm`return value docstring"), + ), + True, + ) + check_return( + results[2], + ( + (0, "-----"), + (1, "Top level docstring "), + (2, ""), + (3, "**Parameters:** "), + (4, "`self` self value docstring "), + (5, ""), + (6, "**Return:** "), + (7, "`norm`return value docstring"), + ), + True, + ) def test_doc_multiline_type_bound_procedure_arg_list(): @@ -159,8 +275,28 @@ def test_doc_multiline_type_bound_procedure_arg_list(): string += hover_request(file_path, 15, 47) errcode, results = run_request(string) assert errcode == 0 - check_return(results[1], ((2, " !! Doc 9"), (3, " !! Doc 10"))) + check_return( + results[1], + ( + (0, "-----"), + (1, "Doc 3 "), + (2, ""), + (3, "**Parameters:** "), + (4, "`arg1` Doc 9"), + (5, "Doc 10"), + ), + True, + ) check_return( results[2], - ((2, " !! Doc 9"), (3, " !! Doc 10"), (5, " !! Doc 11"), (6, " !! Doc 12")), + ( + (0, "-----"), + (1, ""), + (2, "**Parameters:** "), + (3, "`arg1` Doc 9"), + (4, "Doc 10 "), + (5, "`self` Doc 11"), + (6, "Doc 12"), + ), + True, ) diff --git a/test/test_source/docs/test_doxygen.f90 b/test/test_source/docs/test_doxygen.f90 index 3d96cc49..ad4b4afd 100644 --- a/test/test_source/docs/test_doxygen.f90 +++ b/test/test_source/docs/test_doxygen.f90 @@ -25,6 +25,7 @@ end subroutine insert !! \f$\cos \theta = \frac{ \vec v \cdot \vec w}{\abs{v}\abs{w}}\f$. !! !! @param[in] \f$v,w\f$ real vectors + !! size: n !! @return a real value describing the angle. 0 if \f$\abs v\f$ or \f$\abs w\f$ below a !! threshold. pure function calc_angle(v, w) result (theta) diff --git a/test/test_source/subdir/test_free.f90 b/test/test_source/subdir/test_free.f90 index 12e4f804..17fc940f 100644 --- a/test/test_source/subdir/test_free.f90 +++ b/test/test_source/subdir/test_free.f90 @@ -13,7 +13,7 @@ MODULE test_free CONTAINS PROCEDURE :: create => vector_create !< Doc 1 PROCEDURE :: norm => vector_norm !< Doc 2 - PROCEDURE, PASS(self) :: bound_pass => bound_pass + PROCEDURE, PASS(self) :: bound_pass => bound_pass !< Doc 3 END TYPE vector ! TYPE, EXTENDS(vector) :: scaled_vector @@ -48,14 +48,14 @@ FUNCTION vector_norm(self) RESULT(norm) END FUNCTION vector_norm !> Doc 7 SUBROUTINE scaled_vector_set(self, scale) -CLASS(scaled_vector), INTENT(inout) :: self +CLASS(scaled_vector), INTENT(inout) :: self ! no documentation REAL(8), INTENT(in) :: scale !< Doc 8 self%scale%val = scale END SUBROUTINE scaled_vector_set -!> +!> Top level docstring FUNCTION scaled_vector_norm(self) RESULT(norm) -CLASS(scaled_vector), INTENT(in) :: self -REAL(8) :: norm +CLASS(scaled_vector), INTENT(in) :: self !< self value docstring +REAL(8) :: norm !< return value docstring norm = self%scale%val*SQRT(DOT_PRODUCT(self%v,self%v)) END FUNCTION scaled_vector_norm ! From b4e954b681c4626975c06d614262955258c89da5 Mon Sep 17 00:00:00 2001 From: gnikit Date: Mon, 31 Oct 2022 17:22:59 +0000 Subject: [PATCH 04/14] feat: make Sub/function signature help to Markdown --- fortls/helper_functions.py | 28 +++++++++++++++++ fortls/langserver.py | 16 ++++------ fortls/objects.py | 24 +++++++++++--- test/test_server_signature_help.py | 49 +++++++++++++++++++++++++++++ test/test_source/signature/help.f90 | 28 +++++++++++++++++ 5 files changed, 130 insertions(+), 15 deletions(-) create mode 100644 test/test_source/signature/help.f90 diff --git a/fortls/helper_functions.py b/fortls/helper_functions.py index 16b80696..9275fd50 100644 --- a/fortls/helper_functions.py +++ b/fortls/helper_functions.py @@ -580,3 +580,31 @@ def get_var_stack(line: str) -> list[str]: return final_op_split[-1].split("%") else: return None + + +def fortran_md(code: str, docs: str | None, highlight: bool, langid: str = "fortran90"): + """Convert Fortran code to markdown + + Parameters + ---------- + code : str + Fortran code + docs : str | None + Documentation string, only makes sense if ``highlight`` is ``True`` + highlight : bool + Whether to highlight the code + langid : str, optional + Language ID, by default 'fortran90' + + Returns + ------- + str + Markdown string + """ + msg = code + if highlight: + msg = f"```{langid}\n{code}\n```" + # Add documentation + if docs: # if docs is not None or "" + msg += f"\n-----\n{docs}" + return msg diff --git a/fortls/langserver.py b/fortls/langserver.py index 6d4d0c87..56209708 100644 --- a/fortls/langserver.py +++ b/fortls/langserver.py @@ -39,6 +39,7 @@ only_dirs, resolve_globs, set_keyword_ordering, + fortran_md ) from fortls.intrinsics import ( Intrinsic, @@ -1037,12 +1038,9 @@ def serve_definition(self, request: dict): def serve_hover(self, request: dict): def create_hover(string: str, docs: str | None, fortran: bool): - msg = string - if fortran: - msg = f"```{self.hover_language}\n{string}\n```" - if docs: # if docs is not None or "" - msg += f"\n-----\n{docs}" - return msg + # This does not account for Fixed Form Fortran, but it should be + # okay for 99% of cases + return fortran_md(string, docs, fortran, self.hover_language) def create_signature_hover(): sig_request = request.copy() @@ -1082,8 +1080,7 @@ def create_signature_hover(): var_type: int = var_obj.get_type() hover_array = [] if var_type in (SUBROUTINE_TYPE_ID, FUNCTION_TYPE_ID): - hover_str, docs, highlight = var_obj.get_hover(long=True) - hover_array.append(create_hover(hover_str, docs, highlight)) + hover_array.append(var_obj.get_hover_md(long=True)) elif var_type == INTERFACE_TYPE_ID: for member in var_obj.mems: hover_str, docs, highlight = member.get_hover(long=True) @@ -1093,8 +1090,7 @@ def create_signature_hover(): # Unless we have a Fortran literal include the desc in the hover msg # See get_definition for an explanation about this default name if not var_obj.desc.startswith(FORTRAN_LITERAL): - hover_str, docs, highlight = var_obj.get_hover() - hover_array.append(create_hover(hover_str, docs, highlight)) + hover_array.append(var_obj.get_hover_md(long=True)) # Hover for Literal variables elif var_obj.desc.endswith("REAL"): hover_array.append(create_hover("REAL", None, True)) diff --git a/fortls/objects.py b/fortls/objects.py index 52c09a52..eca509cb 100644 --- a/fortls/objects.py +++ b/fortls/objects.py @@ -27,7 +27,12 @@ FRegex, ) from fortls.ftypes import IncludeInfo, UseInfo -from fortls.helper_functions import get_keywords, get_paren_substring, get_var_stack +from fortls.helper_functions import ( + fortran_md, + get_keywords, + get_paren_substring, + get_var_stack, +) from fortls.json_templates import diagnostic_json, location_json, range_json from fortls.jsonrpc import path_to_uri @@ -410,7 +415,9 @@ def get_documentation(self): return self.doc_str def get_hover(self, long=False, drop_arg=-1) -> tuple[str | None, str | None, bool]: - return None, None, False + + def get_hover_md(self, long=False, drop_arg=-1) -> str: + return "" def get_signature(self, drop_arg=-1): return None, None, None @@ -928,6 +935,9 @@ def get_hover(self, long=False, drop_arg=-1): hover_array, docs = self.get_docs_full(hover_array, long, drop_arg) return "\n ".join(hover_array), " \n".join(docs), long + def get_hover_md(self, long=False, drop_arg=-1): + return fortran_md(*self.get_hover(long, drop_arg)) + def get_docs_full( self, hover_array: list[str], long=False, drop_arg=-1 ) -> tuple[list[str], list[str]]: @@ -981,9 +991,10 @@ def get_signature(self, drop_arg=-1): label = f"{arg_obj.name.lower()}={arg_obj.name.lower()}" else: label = arg_obj.name.lower() - arg_sigs.append( - {"label": label, "documentation": arg_obj.get_hover()[0]} - ) + msg = arg_obj.get_hover_md() + # Create MarkupContent object + msg = {"kind": "markdown", "value": msg} + arg_sigs.append({"label": label, "documentation": msg}) call_sig, _ = self.get_snippet() return call_sig, self.get_documentation(), arg_sigs @@ -1694,6 +1705,9 @@ def get_hover(self, long=False, drop_arg=-1) -> tuple[str, str, bool]: hover_str += f" = {self.param_val}" return hover_str, doc_str, True + def get_hover_md(self, long=False, drop_arg=-1): + return fortran_md(*self.get_hover(long, drop_arg)) + def get_keywords(self): # TODO: if local keywords are set they should take precedence over link_obj # Alternatively, I could do a dictionary merge with local variables diff --git a/test/test_server_signature_help.py b/test/test_server_signature_help.py index 741a1ff7..7e3375ce 100644 --- a/test/test_server_signature_help.py +++ b/test/test_server_signature_help.py @@ -64,3 +64,52 @@ def test_intrinsics(): assert len(ref) == len(results) - 1 for i, r in enumerate(ref): validate_sigh(results[i + 1], r) + + +def test_subroutine_markdown(): + """Test that the signature help is correctly resolved for all arguments and + that the autocompletion is correct for the subroutine signature, when there + is documentation present. + """ + string = write_rpc_request( + 1, "initialize", {"rootPath": str(test_dir / "signature")} + ) + file_path = test_dir / "signature" / "help.f90" + string += sigh_request(file_path, 22, 18) + errcode, results = run_request( + string, ["--hover_signature", "--use_signature_help", "-n1"] + ) + assert errcode == 0 + # Compare against the full signature help response + ref = { + "signatures": [ + { + "label": "sub2call(arg1, arg2=arg2)", + "parameters": [ + { + "label": "arg1", + "documentation": { + "kind": "markdown", + "value": ( + "```fortran90\nINTEGER, INTENT(IN) ::" + " arg1\n```\n-----\nDoc for arg1" + ), + }, + }, + { + "label": "arg2=arg2", + "documentation": { + "kind": "markdown", + "value": ( + "```fortran90\nINTEGER, INTENT(IN), OPTIONAL ::" + " arg2\n```\n-----\nDoc for arg2" + ), + }, + }, + ], + "documentation": "Top level Doc", + } + ], + "activeParameter": 0, + } + assert results[1] == ref diff --git a/test/test_source/signature/help.f90 b/test/test_source/signature/help.f90 new file mode 100644 index 00000000..a7b07f38 --- /dev/null +++ b/test/test_source/signature/help.f90 @@ -0,0 +1,28 @@ +module sig_help_markdown + implicit none + private + +contains + !> Top level Doc + subroutine sub2call(arg1, arg2) + integer, intent(in) :: arg1 !< Doc for arg1 + integer, intent(in), optional :: arg2 !< Doc for arg2 + print*, "sub2call: arg1=", arg1 + if (present(arg2)) print*, "sub2call: arg2=", arg2 + end subroutine sub2call + + !> Top level Doc + function fun2fcall(arg1, arg2) result(res) + integer, intent(in) :: arg1 !< Doc for arg1 + integer, intent(in), optional :: arg2 !< Doc for arg2 + integer :: res + res = arg1 + if (present(arg2)) res = res + arg2 + end function fun2fcall + + subroutine calling() + call sub2call(1, 2) + print*, "fun2fcall(1, 2)=", fun2fcall(1, 2) + end subroutine calling + +end module sig_help_markdown \ No newline at end of file From e03d6081d51dd8091e2fd59114dac2a48aa11c32 Mon Sep 17 00:00:00 2001 From: gnikit Date: Mon, 31 Oct 2022 17:23:31 +0000 Subject: [PATCH 05/14] chore: fixing typings --- fortls/parse_fortran.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fortls/parse_fortran.py b/fortls/parse_fortran.py index 3c070d09..c40d1c76 100644 --- a/fortls/parse_fortran.py +++ b/fortls/parse_fortran.py @@ -13,7 +13,7 @@ except ImportError: from typing_extensions import Literal -from re import Match, Pattern +from re import Pattern, Match from fortls.constants import ( DO_TYPE_ID, @@ -1980,7 +1980,7 @@ def get_single_line_docstring(self, line: str) -> list[str]: doc = line[match.end(0) :].strip() return [doc] if doc else [] - def get_comment_regexs(self) -> tuple[Pattern, Pattern]: + def get_comment_regexs(self) -> tuple[Pattern[str], Pattern[str]]: if self.fixed: return FRegex.FIXED_COMMENT, FRegex.FIXED_DOC return FRegex.FREE_COMMENT, FRegex.FREE_DOC From 6336e5ce2b116055d037c84767946fb802ba833a Mon Sep 17 00:00:00 2001 From: gnikit Date: Mon, 31 Oct 2022 17:23:44 +0000 Subject: [PATCH 06/14] chore: fix typings --- test/test_server_hover.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/test_server_hover.py b/test/test_server_hover.py index 4bbd4fe1..5854c8b2 100644 --- a/test/test_server_hover.py +++ b/test/test_server_hover.py @@ -1,7 +1,7 @@ -from setup_tests import run_request, test_dir, write_rpc_request +from setup_tests import run_request, test_dir, write_rpc_request, Path -def hover_req(file_path: str, ln: int, col: int) -> str: +def hover_req(file_path: Path, ln: int, col: int) -> str: return write_rpc_request( 1, "textDocument/hover", @@ -336,7 +336,7 @@ def test_hover_interface_as_argument(): string += hover_req(file_path, 19, 14) errcode, results = run_request(string, fortls_args=["--sort_keywords", "-n1"]) assert errcode == 0 - ref_results = ( + ref_results = [ # Could be subject to change """```fortran90 FUNCTION foo2(f, g, h) RESULT(arg3) @@ -345,7 +345,7 @@ def test_hover_interface_as_argument(): FUNCTION h(x) REAL :: arg3 ```""", - ) + ] validate_hover(results, ref_results) From 5b02cdfd1a017e4635da9920d9212836ca5f18ab Mon Sep 17 00:00:00 2001 From: gnikit Date: Mon, 31 Oct 2022 17:29:03 +0000 Subject: [PATCH 07/14] fix: add back function def --- fortls/objects.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/fortls/objects.py b/fortls/objects.py index eca509cb..45943d47 100644 --- a/fortls/objects.py +++ b/fortls/objects.py @@ -415,6 +415,7 @@ def get_documentation(self): return self.doc_str def get_hover(self, long=False, drop_arg=-1) -> tuple[str | None, str | None, bool]: + return None, None, False def get_hover_md(self, long=False, drop_arg=-1) -> str: return "" @@ -1591,6 +1592,7 @@ def __init__( var_desc: str, keywords: list, keyword_info: dict = None, + # kind: int | str = None, link_obj=None, ): super().__init__() @@ -1612,6 +1614,7 @@ def __init__( self.is_external: bool = False self.param_val: str = None self.link_name: str = None + # self.kind: int | str = kind self.FQSN: str = self.name.lower() if link_obj is not None: self.link_name = link_obj.lower() From f3217fba07ec1d91b857b93cc8dc352904e0753c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 31 Oct 2022 17:29:22 +0000 Subject: [PATCH 08/14] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- fortls/langserver.py | 2 +- fortls/parse_fortran.py | 2 +- test/test_server_hover.py | 2 +- test/test_source/signature/help.f90 | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/fortls/langserver.py b/fortls/langserver.py index 56209708..a925f62b 100644 --- a/fortls/langserver.py +++ b/fortls/langserver.py @@ -33,13 +33,13 @@ ) from fortls.helper_functions import ( expand_name, + fortran_md, get_line_prefix, get_paren_level, get_var_stack, only_dirs, resolve_globs, set_keyword_ordering, - fortran_md ) from fortls.intrinsics import ( Intrinsic, diff --git a/fortls/parse_fortran.py b/fortls/parse_fortran.py index c40d1c76..76fa3cc5 100644 --- a/fortls/parse_fortran.py +++ b/fortls/parse_fortran.py @@ -13,7 +13,7 @@ except ImportError: from typing_extensions import Literal -from re import Pattern, Match +from re import Match, Pattern from fortls.constants import ( DO_TYPE_ID, diff --git a/test/test_server_hover.py b/test/test_server_hover.py index 5854c8b2..272ad8dc 100644 --- a/test/test_server_hover.py +++ b/test/test_server_hover.py @@ -1,4 +1,4 @@ -from setup_tests import run_request, test_dir, write_rpc_request, Path +from setup_tests import Path, run_request, test_dir, write_rpc_request def hover_req(file_path: Path, ln: int, col: int) -> str: diff --git a/test/test_source/signature/help.f90 b/test/test_source/signature/help.f90 index a7b07f38..7431d1e1 100644 --- a/test/test_source/signature/help.f90 +++ b/test/test_source/signature/help.f90 @@ -25,4 +25,4 @@ subroutine calling() print*, "fun2fcall(1, 2)=", fun2fcall(1, 2) end subroutine calling -end module sig_help_markdown \ No newline at end of file +end module sig_help_markdown From 0f868031ebdc5c5ba3ecfb559f7a33b1d2164754 Mon Sep 17 00:00:00 2001 From: gnikit Date: Mon, 31 Oct 2022 17:41:48 +0000 Subject: [PATCH 09/14] fix(test): line numbering --- test/test_server_signature_help.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_server_signature_help.py b/test/test_server_signature_help.py index 7e3375ce..14daa442 100644 --- a/test/test_server_signature_help.py +++ b/test/test_server_signature_help.py @@ -75,7 +75,7 @@ def test_subroutine_markdown(): 1, "initialize", {"rootPath": str(test_dir / "signature")} ) file_path = test_dir / "signature" / "help.f90" - string += sigh_request(file_path, 22, 18) + string += sigh_request(file_path, 23, 18) errcode, results = run_request( string, ["--hover_signature", "--use_signature_help", "-n1"] ) From cca7c734c95136ad300c2d54855010c5bb98ec7e Mon Sep 17 00:00:00 2001 From: gnikit Date: Tue, 1 Nov 2022 14:49:11 +0000 Subject: [PATCH 10/14] fix(parser): scope start for line continuation (&) Fixes bug(parser): multiline scope registration assigns end of line not start of line as scope start #217 --- fortls/parse_fortran.py | 13 ++++++++++--- test/test_parser.py | 16 ++++++++++++++++ test/test_server_hover.py | 17 +++++++++++++++++ test/test_source/hover/functions.f90 | 6 ++++++ test/test_source/parse/line_continuations.f90 | 4 ++++ 5 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 test/test_parser.py create mode 100644 test/test_source/parse/line_continuations.f90 diff --git a/fortls/parse_fortran.py b/fortls/parse_fortran.py index 76fa3cc5..7d91b395 100644 --- a/fortls/parse_fortran.py +++ b/fortls/parse_fortran.py @@ -1260,6 +1260,7 @@ def parse( pp_defines = [] line_no = 0 + line_no_end = 0 block_id_stack = [] docs: list[str] = [] # list used to temporarily store docstrings counters = Counter( @@ -1271,13 +1272,16 @@ def parse( ) multi_lines = deque() self.COMMENT_LINE_MATCH, self.DOC_COMMENT_MATCH = self.get_comment_regexs() - while (line_no < self.nLines) or multi_lines: + while (line_no_end < self.nLines) or multi_lines: # Get next line # Get a normal line, i.e. the stack is empty if not multi_lines: + # Check if we need to advance the line number due to `&` continuation + line_no = line_no_end if line_no_end > line_no else line_no # get_line has a 0-based index line = self.get_line(line_no, pp_content=True) - line_no += 1 + line_no += 1 # Move to next line + line_no_end = line_no get_full = True # Line is part of a multi-line construct, i.e. contained ';' else: @@ -1292,6 +1296,7 @@ def parse( idx = self.parse_docs(line, line_no, file_ast, docs) if idx: line_no = idx + line_no_end = line_no continue # Handle preprocessing regions do_skip = False @@ -1309,7 +1314,9 @@ def parse( _, line, post_lines = self.get_code_line( line_no - 1, backward=False, pp_content=True ) - line_no += len(post_lines) + # Save the end of the line for the next iteration. + # Need to keep the line number for registering start of Scopes + line_no_end += len(post_lines) line = "".join([line] + post_lines) line, line_label = strip_line_label(line) line_stripped = strip_strings(line, maintain_len=True) diff --git a/test/test_parser.py b/test/test_parser.py new file mode 100644 index 00000000..8ee2bc83 --- /dev/null +++ b/test/test_parser.py @@ -0,0 +1,16 @@ +from setup_tests import test_dir + +from fortls.parse_fortran import FortranFile + + +def test_line_continuations(): + file_path = test_dir / "parse" / "line_continuations.f90" + file = FortranFile(str(file_path)) + err_str, _ = file.load_from_disk() + assert err_str is None + try: + file.parse() + assert True + except Exception as e: + print(e) + assert False diff --git a/test/test_server_hover.py b/test/test_server_hover.py index 272ad8dc..1ccbc919 100644 --- a/test/test_server_hover.py +++ b/test/test_server_hover.py @@ -505,3 +505,20 @@ def test_intent(): """```fortran90\nREAL, OPTIONAL, INTENT(IN) :: arg5\n```""", ] validate_hover(results, ref_results) + + +def test_multiline_func_args(): + string = write_rpc_request(1, "initialize", {"rootPath": str(test_dir / "hover")}) + file_path = test_dir / "hover" / "functions.f90" + string += hover_req(file_path, 58, 22) + string += hover_req(file_path, 59, 22) + string += hover_req(file_path, 60, 22) + + errcode, results = run_request(string, fortls_args=["-n", "1"]) + assert errcode == 0 + ref_results = [ + "```fortran90\nINTEGER, INTENT(IN) :: val1\n```", + "```fortran90\nINTEGER, INTENT(IN) :: val2\n```", + "```fortran90\nREAL :: val4\n```", + ] + validate_hover(results, ref_results) diff --git a/test/test_source/hover/functions.f90 b/test/test_source/hover/functions.f90 index 38f21916..4292ff9f 100644 --- a/test/test_source/hover/functions.f90 +++ b/test/test_source/hover/functions.f90 @@ -55,3 +55,9 @@ function fun7() result(val) type(c_ptr) :: val val = c_loc(ar) end function fun7 + +real function foobar(val1, & + val2) & + result(val4) +integer, intent(in) :: val1, val2 +end function foobar diff --git a/test/test_source/parse/line_continuations.f90 b/test/test_source/parse/line_continuations.f90 new file mode 100644 index 00000000..2277f119 --- /dev/null +++ b/test/test_source/parse/line_continuations.f90 @@ -0,0 +1,4 @@ +subroutine parse_line_continuations + call report_test("[adaptivity output]", .false., .false., "Congratulations! & + & The output from adaptivity might even be OK if you get this far.") +end subroutine parse_line_continuations From 105b6ace701ea6b7fb4e9fec3092e4ee65ebb94f Mon Sep 17 00:00:00 2001 From: gnikit Date: Tue, 1 Nov 2022 16:13:37 +0000 Subject: [PATCH 11/14] chore: remove quotes to make the link clickable --- fortls/langserver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fortls/langserver.py b/fortls/langserver.py index a925f62b..55424f23 100644 --- a/fortls/langserver.py +++ b/fortls/langserver.py @@ -1448,7 +1448,7 @@ def workspace_init(self): result_obj = result.get() if isinstance(result_obj, str): self.post_message( - f"Initialization failed for file '{path}': {result_obj}" + f"Initialization failed for file {path}: {result_obj}" ) continue self.workspace[path] = result_obj From c3f805af5bd9888d887efde0a56bdaf410ce8d8e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 17 Oct 2022 17:50:13 +0000 Subject: [PATCH 12/14] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.0.0 → v3.1.0](https://github.com/asottile/pyupgrade/compare/v3.0.0...v3.1.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 005ed8ef..530832fd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/asottile/pyupgrade - rev: v3.0.0 + rev: v3.1.0 hooks: - id: pyupgrade - repo: https://github.com/pycqa/isort From 1f503191ff24aeade46bb8c51a01028289e809cd Mon Sep 17 00:00:00 2001 From: gnikit Date: Sat, 5 Nov 2022 20:51:26 +0000 Subject: [PATCH 13/14] refactor(hover): remove unused sig help The hover response included a snippet for a signature request which is not how signature help requests are dealt. --- fortls/langserver.py | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/fortls/langserver.py b/fortls/langserver.py index 55424f23..ae650da2 100644 --- a/fortls/langserver.py +++ b/fortls/langserver.py @@ -1042,27 +1042,6 @@ def create_hover(string: str, docs: str | None, fortran: bool): # okay for 99% of cases return fortran_md(string, docs, fortran, self.hover_language) - def create_signature_hover(): - sig_request = request.copy() - sig_result = self.serve_signature(sig_request) - try: - arg_id = sig_result.get("activeParameter") - if arg_id is not None: - arg_info = sig_result["signatures"][0]["parameters"][arg_id] - arg_doc = arg_info["documentation"] - doc_split = arg_doc.find("\n !!") - if doc_split < 0: - arg_string = f"{arg_doc} :: {arg_info['label']}" - else: - arg_string = ( - f"{arg_doc[:doc_split]} :: " - f"{arg_info['label']}{arg_doc[doc_split:]}" - ) - # TODO: check if correct. I think it's not - return create_hover(arg_string, None, True) - except: - pass - # Get parameters from request params: dict = request["params"] uri: str = params["textDocument"]["uri"] @@ -1102,11 +1081,6 @@ def create_signature_hover(): hover_str = f"CHARACTER(LEN={len(var_obj.name)-2})" hover_array.append(create_hover(hover_str, None, True)) - # Include the signature if one is present e.g. if in an argument list - if self.hover_signature: - hover_str: str | None = create_signature_hover() - if hover_str is not None: - hover_array.append(hover_str) if len(hover_array) > 0: return {"contents": {"kind": "markdown", "value": "\n".join(hover_array)}} return None From 6f531f2595063ffc17b0fc4a4c60699499fc2360 Mon Sep 17 00:00:00 2001 From: gnikit Date: Sun, 6 Nov 2022 23:03:02 +0000 Subject: [PATCH 14/14] feat: add Markdown syntax in signature help Now the documentation of the procedure will be interpreted as Markdown similar to hover --- fortls/langserver.py | 2 +- test/test_server_signature_help.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fortls/langserver.py b/fortls/langserver.py index ae650da2..dedcaefe 100644 --- a/fortls/langserver.py +++ b/fortls/langserver.py @@ -894,7 +894,7 @@ def check_optional(arg, params: dict): param_num = opt_num signature = {"label": label, "parameters": params} if doc_str is not None: - signature["documentation"] = doc_str + signature["documentation"] = {"kind": "markdown", "value": doc_str} req_dict = {"signatures": [signature], "activeParameter": param_num} return req_dict diff --git a/test/test_server_signature_help.py b/test/test_server_signature_help.py index 14daa442..34c2565f 100644 --- a/test/test_server_signature_help.py +++ b/test/test_server_signature_help.py @@ -107,7 +107,7 @@ def test_subroutine_markdown(): }, }, ], - "documentation": "Top level Doc", + "documentation": {"kind": "markdown", "value": "Top level Doc"}, } ], "activeParameter": 0,