From b470109e8455df7607bef743e86e0192047a9e34 Mon Sep 17 00:00:00 2001 From: gnikit Date: Tue, 8 Nov 2022 19:56:52 +0000 Subject: [PATCH 1/4] feat: Change autocomplete to use MarkupContent Fixes #219 --- CHANGELOG.md | 3 ++ fortls/langserver.py | 9 ++++-- test/test_server_completion.py | 56 ++++++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f5ca1ed0..57f44eef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ ### Changed +- Changed the completion signature to include the full Markdown documentation + for the completion item. + ([#219](https://github.com/gnikit/fortls/issues/219)) - Changed hover messages and signature help to use Markdown ([#45](https://github.com/gnikit/fortls/issues/45)) diff --git a/fortls/langserver.py b/fortls/langserver.py index dedcaefe..ef9d8406 100644 --- a/fortls/langserver.py +++ b/fortls/langserver.py @@ -488,12 +488,15 @@ def build_comp( comp_obj["kind"] = map_types(candidate.get_type()) if is_member and (comp_obj["kind"] == 3): comp_obj["kind"] = 2 + # Detail label shown above documentation, also shown when + # documentation is collapsed i.e. short form completions comp_obj["detail"] = candidate.get_desc() if call_sig is not None: comp_obj["detail"] += " " + call_sig - # TODO: doc_str should probably be appended, see LSP standard - hover_msg, doc_str, _ = candidate.get_hover() - if hover_msg is not None: + # Use the full markdown documentation + hover_msg = candidate.get_hover_md(long=True) + if hover_msg: + hover_msg = {"kind": "markdown", "value": hover_msg} comp_obj["documentation"] = hover_msg return comp_obj diff --git a/test/test_server_completion.py b/test/test_server_completion.py index 15ff05c4..d904aac1 100644 --- a/test/test_server_completion.py +++ b/test/test_server_completion.py @@ -321,3 +321,59 @@ def test_comp_fixed(): assert len(exp_results) == len(results) - 1 for i, ref in enumerate(exp_results): validate_comp(results[i + 1], ref) + + +def test_comp_documentation(): + """Test that "documentation" is returned for autocomplete results.""" + string = write_rpc_request(1, "initialize", {"rootPath": str(test_dir)}) + file_path = test_dir / "subdir" / "test_free.f90" + string += comp_request(file_path, 21, 37) + errcode, results = run_request( + string, + ) + assert errcode == 0 + + exp_results = [ + { + "label": "scaled_vector_set", + "kind": 3, + "detail": "SUBROUTINE", + "documentation": { + "kind": "markdown", + "value": ( + "```fortran90\n" + "SUBROUTINE scaled_vector_set(self, scale)\n" + " CLASS(scaled_vector), INTENT(INOUT) :: self\n" + " REAL(8), INTENT(IN) :: scale\n" + "```\n" + "-----\n" + "Doc 7 \n\n" + "**Parameters:** \n" + "`scale` Doc 8" + ), + }, + }, + { + "label": "scaled_vector_norm", + "kind": 3, + "detail": "REAL(8) FUNCTION", + "documentation": { + "kind": "markdown", + "value": ( + "```fortran90\n" + "FUNCTION scaled_vector_norm(self) RESULT(norm)\n" + " CLASS(scaled_vector), INTENT(IN) :: self\n" + " REAL(8) :: norm\n" + "```\n" + "-----\n" + "Top level docstring \n\n" + "**Parameters:** \n" + "`self` self value docstring \n\n" + "**Return:** \n" + "`norm`return value docstring" + ), + }, + }, + ] + assert len(exp_results) == len(results[1]) + assert exp_results == results[1] From 18a1b9759993995e8a4f735490ae342d19834902 Mon Sep 17 00:00:00 2001 From: gnikit Date: Tue, 8 Nov 2022 23:34:06 +0000 Subject: [PATCH 2/4] refactor: remove highlight argument from hover Also, added a unittest for intrinsic hover --- fortls/helper_functions.py | 8 +++----- fortls/intrinsics.py | 9 +++++++-- fortls/langserver.py | 16 ++++++++-------- fortls/objects.py | 32 +++++++++++++++----------------- test/test_server_hover.py | 14 ++++++++++++++ 5 files changed, 47 insertions(+), 32 deletions(-) diff --git a/fortls/helper_functions.py b/fortls/helper_functions.py index 9275fd50..4add14cf 100644 --- a/fortls/helper_functions.py +++ b/fortls/helper_functions.py @@ -582,7 +582,7 @@ def get_var_stack(line: str) -> list[str]: return None -def fortran_md(code: str, docs: str | None, highlight: bool, langid: str = "fortran90"): +def fortran_md(code: str, docs: str | None, langid: str = "fortran90"): """Convert Fortran code to markdown Parameters @@ -590,9 +590,7 @@ def fortran_md(code: str, docs: str | None, highlight: bool, langid: str = "fort code : str Fortran code docs : str | None - Documentation string, only makes sense if ``highlight`` is ``True`` - highlight : bool - Whether to highlight the code + Documentation string langid : str, optional Language ID, by default 'fortran90' @@ -602,7 +600,7 @@ def fortran_md(code: str, docs: str | None, highlight: bool, langid: str = "fort Markdown string """ msg = code - if highlight: + if code: msg = f"```{langid}\n{code}\n```" # Add documentation if docs: # if docs is not None or "" diff --git a/fortls/intrinsics.py b/fortls/intrinsics.py index 45525ec2..48884984 100644 --- a/fortls/intrinsics.py +++ b/fortls/intrinsics.py @@ -1,7 +1,7 @@ import json import os -from fortls.helper_functions import map_keywords +from fortls.helper_functions import fortran_md, map_keywords from fortls.objects import ( FortranAST, FortranObj, @@ -71,7 +71,12 @@ def get_signature(self): return call_sig, self.doc_str, arg_sigs def get_hover(self, long=False): - return self.doc_str, None, False + return None, self.doc_str + + def get_hover_md(self, long=False): + msg, docs = self.get_hover(long) + msg = msg if msg else "" + return fortran_md(msg, docs) def is_callable(self): if self.type == 2: diff --git a/fortls/langserver.py b/fortls/langserver.py index ef9d8406..ef515f42 100644 --- a/fortls/langserver.py +++ b/fortls/langserver.py @@ -1040,10 +1040,10 @@ def serve_definition(self, request: dict): return None def serve_hover(self, request: dict): - def create_hover(string: str, docs: str | None, fortran: bool): + def create_hover(string: str, docs: str | None): # 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) + return fortran_md(string, docs, self.hover_language) # Get parameters from request params: dict = request["params"] @@ -1065,9 +1065,9 @@ def create_hover(string: str, docs: str | None, fortran: bool): 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) + hover_str, docs = member.get_hover(long=True) if hover_str is not None: - hover_array.append(create_hover(hover_str, docs, highlight)) + hover_array.append(create_hover(hover_str, docs)) 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 @@ -1075,14 +1075,14 @@ def create_hover(string: str, docs: str | None, fortran: bool): 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)) + hover_array.append(create_hover("REAL", None)) elif var_obj.desc.endswith("INTEGER"): - hover_array.append(create_hover("INTEGER", None, True)) + hover_array.append(create_hover("INTEGER", None)) elif var_obj.desc.endswith("LOGICAL"): - hover_array.append(create_hover("LOGICAL", None, True)) + hover_array.append(create_hover("LOGICAL", None)) elif var_obj.desc.endswith("STRING"): hover_str = f"CHARACTER(LEN={len(var_obj.name)-2})" - hover_array.append(create_hover(hover_str, None, True)) + hover_array.append(create_hover(hover_str, None)) if len(hover_array) > 0: return {"contents": {"kind": "markdown", "value": "\n".join(hover_array)}} diff --git a/fortls/objects.py b/fortls/objects.py index 45943d47..ce0ccd1c 100644 --- a/fortls/objects.py +++ b/fortls/objects.py @@ -414,8 +414,8 @@ def get_placeholders(arg_list: list[str]): 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(self, long=False, drop_arg=-1) -> tuple[str | None, str | None]: + return None, None def get_hover_md(self, long=False, drop_arg=-1) -> str: return "" @@ -934,7 +934,7 @@ def get_hover(self, long=False, drop_arg=-1): keyword_list.append(f"{self.get_desc()} ") hover_array = [" ".join(keyword_list) + sub_sig] hover_array, docs = self.get_docs_full(hover_array, long, drop_arg) - return "\n ".join(hover_array), " \n".join(docs), long + return "\n ".join(hover_array), " \n".join(docs) def get_hover_md(self, long=False, drop_arg=-1): return fortran_md(*self.get_hover(long, drop_arg)) @@ -969,7 +969,7 @@ def get_docs_full( for i, arg_obj in enumerate(self.arg_objs): if arg_obj is None or i == drop_arg: continue - arg, doc_str, _ = arg_obj.get_hover() + arg, doc_str = arg_obj.get_hover() hover_array.append(arg) if doc_str: # If doc_str is not None or "" if has_args: @@ -1007,7 +1007,7 @@ def get_interface_array( for i, arg_obj in enumerate(self.arg_objs): if arg_obj is None: return None - arg_doc, docs, _ = arg_obj.get_hover() + arg_doc, docs = arg_obj.get_hover() if i == change_arg: i0 = arg_doc.lower().find(change_strings[0].lower()) if i0 >= 0: @@ -1122,9 +1122,7 @@ def get_desc(self): def is_callable(self): return False - def get_hover( - self, long: bool = False, drop_arg: int = -1 - ) -> tuple[str, str, bool]: + def get_hover(self, long: bool = False, drop_arg: int = -1) -> tuple[str, str]: """Construct the hover message for a FUNCTION. Two forms are produced here the `long` i.e. the normal for hover requests @@ -1162,7 +1160,7 @@ def get_hover( # Only append the return value if using long form if self.result_obj and long: # Parse the documentation from the result variable - arg_doc, doc_str, _ = self.result_obj.get_hover() + 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) @@ -1170,7 +1168,7 @@ def get_hover( 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), " \n".join(docs), long + return "\n ".join(hover_array), " \n".join(docs) # TODO: fix this def get_interface(self, name_replace=None, change_arg=-1, change_strings=None): @@ -1187,7 +1185,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, docs, _ = self.result_obj.get_hover() + 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: @@ -1696,7 +1694,7 @@ def get_snippet(self, name_replace=None, drop_arg=-1): # Normal variable return None, None - def get_hover(self, long=False, drop_arg=-1) -> tuple[str, str, bool]: + def get_hover(self, long=False, drop_arg=-1) -> tuple[str, str]: doc_str = self.get_documentation() # In associated blocks we need to fetch the desc and keywords of the # linked object @@ -1706,7 +1704,7 @@ def get_hover(self, long=False, drop_arg=-1) -> tuple[str, str, bool]: hover_str += f" :: {self.name}" if self.is_parameter() and self.param_val: hover_str += f" = {self.param_val}" - return hover_str, doc_str, True + return hover_str, doc_str def get_hover_md(self, long=False, drop_arg=-1): return fortran_md(*self.get_hover(long, drop_arg)) @@ -1845,17 +1843,17 @@ def get_documentation(self): return self.link_obj.get_documentation() return self.doc_str - def get_hover(self, long=False, drop_arg=-1) -> tuple[str, str, bool]: + def get_hover(self, long=False, drop_arg=-1) -> tuple[str, str]: docs = self.get_documentation() if not long: hover_str = ", ".join([self.desc] + get_keywords(self.keywords)) - return hover_str, docs, True + return hover_str, docs # 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( + 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 @@ -1874,7 +1872,7 @@ def get_hover(self, long=False, drop_arg=-1) -> tuple[str, str, bool]: if docs is None: docs = "" docs += " \n" + link_docs - return hover_str, docs, True + return hover_str, docs def get_signature(self, drop_arg=-1): if self.link_obj is not None: diff --git a/test/test_server_hover.py b/test/test_server_hover.py index 1ccbc919..bca04fed 100644 --- a/test/test_server_hover.py +++ b/test/test_server_hover.py @@ -522,3 +522,17 @@ def test_multiline_func_args(): "```fortran90\nREAL :: val4\n```", ] validate_hover(results, ref_results) + + +def test_intrinsics(): + string = write_rpc_request(1, "initialize", {"rootPath": str(test_dir / "hover")}) + file_path = test_dir / "hover" / "functions.f90" + string += hover_req(file_path, 39, 23) + errcode, results = run_request(string, fortls_args=["-n", "1"]) + assert errcode == 0 + ref_results = [ + "\n-----\nSIZE(ARRAY,DIM=dim,KIND=kind) determines the extent of ARRAY along a" + " specified dimension DIM, or the total number of elements in ARRAY if DIM is" + " absent." + ] + validate_hover(results, ref_results) From 17719f3f929000a92c1a5981e7d8fa478811eb86 Mon Sep 17 00:00:00 2001 From: gnikit Date: Tue, 8 Nov 2022 23:46:41 +0000 Subject: [PATCH 3/4] refactor: make get_hover_md call get_hover Default behaviour makes the Markdown hover signature to be a function of the general hover message --- fortls/helper_functions.py | 2 +- fortls/objects.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/fortls/helper_functions.py b/fortls/helper_functions.py index 4add14cf..60934a58 100644 --- a/fortls/helper_functions.py +++ b/fortls/helper_functions.py @@ -599,7 +599,7 @@ def fortran_md(code: str, docs: str | None, langid: str = "fortran90"): str Markdown string """ - msg = code + msg = "" if code: msg = f"```{langid}\n{code}\n```" # Add documentation diff --git a/fortls/objects.py b/fortls/objects.py index ce0ccd1c..4ca6d220 100644 --- a/fortls/objects.py +++ b/fortls/objects.py @@ -418,7 +418,8 @@ def get_hover(self, long=False, drop_arg=-1) -> tuple[str | None, str | None]: return None, None def get_hover_md(self, long=False, drop_arg=-1) -> str: - return "" + msg, docs = self.get_hover(long, drop_arg) + return fortran_md(msg, docs) def get_signature(self, drop_arg=-1): return None, None, None From 4462a7691558a50d5b066b0e77b1d08353021225 Mon Sep 17 00:00:00 2001 From: gnikit Date: Wed, 9 Nov 2022 09:52:45 +0000 Subject: [PATCH 4/4] refactor: remove long hover of type-bound procs --- fortls/objects.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/fortls/objects.py b/fortls/objects.py index 4ca6d220..569d40b5 100644 --- a/fortls/objects.py +++ b/fortls/objects.py @@ -1846,9 +1846,6 @@ def get_documentation(self): def get_hover(self, long=False, drop_arg=-1) -> tuple[str, str]: docs = self.get_documentation() - if not long: - hover_str = ", ".join([self.desc] + get_keywords(self.keywords)) - return hover_str, docs # Long hover message if self.link_obj is None: sub_sig, _ = self.get_snippet()