Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Change autocomplete to use MarkupContent #220

Merged
merged 4 commits into from
Nov 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand Down
10 changes: 4 additions & 6 deletions fortls/helper_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -582,17 +582,15 @@ 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
----------
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'

Expand All @@ -601,8 +599,8 @@ def fortran_md(code: str, docs: str | None, highlight: bool, langid: str = "fort
str
Markdown string
"""
msg = code
if highlight:
msg = ""
if code:
msg = f"```{langid}\n{code}\n```"
# Add documentation
if docs: # if docs is not None or ""
Expand Down
9 changes: 7 additions & 2 deletions fortls/intrinsics.py
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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:
Expand Down
25 changes: 14 additions & 11 deletions fortls/langserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -1037,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"]
Expand All @@ -1062,24 +1065,24 @@ 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
if not var_obj.desc.startswith(FORTRAN_LITERAL):
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)}}
Expand Down
36 changes: 16 additions & 20 deletions fortls/objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -414,11 +414,12 @@ 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 ""
msg, docs = self.get_hover(long, drop_arg)
return fortran_md(msg, docs)

def get_signature(self, drop_arg=-1):
return None, None, None
Expand Down Expand Up @@ -934,7 +935,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))
Expand Down Expand Up @@ -969,7 +970,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:
Expand Down Expand Up @@ -1007,7 +1008,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:
Expand Down Expand Up @@ -1122,9 +1123,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

Expand Down Expand Up @@ -1162,15 +1161,15 @@ 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)
# 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), " \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):
Expand All @@ -1187,7 +1186,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:
Expand Down Expand Up @@ -1696,7 +1695,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
Expand All @@ -1706,7 +1705,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))
Expand Down Expand Up @@ -1845,17 +1844,14 @@ 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
# 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
Expand All @@ -1874,7 +1870,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:
Expand Down
56 changes: 56 additions & 0 deletions test/test_server_completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
14 changes: 14 additions & 0 deletions test/test_server_hover.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)