Skip to content

Commit

Permalink
Improve signature printing for language server (#26117)
Browse files Browse the repository at this point in the history
Improves the signature printing for the language server.
`symbol_signature.py` now handles more cases which improves the user
experience when hovering over symbols.

Before this PR
![Screenshot 2024-10-21 at 3 26 47
PM](https://github.com/user-attachments/assets/39889238-d36e-4dc7-a4ea-72e89151e0d1)

After this PR
![Screenshot 2024-10-21 at 3 27 03
PM](https://github.com/user-attachments/assets/04f6f15b-e82b-446c-83f0-8efa6b7420d6)

Note: `symbol_signature.py` continues to be a stopgap since we have no
universal dyno pretty printer yet, see
#24716

[Reviewed by @DanilaFe]
  • Loading branch information
jabraham17 authored Oct 22, 2024
2 parents 25e0d33 + 3f09bff commit 0700b68
Show file tree
Hide file tree
Showing 2 changed files with 166 additions and 2 deletions.
135 changes: 133 additions & 2 deletions tools/chpl-language-server/src/symbol_signature.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
# limitations under the License.
#

from typing import List, Optional, Union
from typing import List, Optional, Union, Iterable
import chapel
from dataclasses import dataclass
import enum
Expand Down Expand Up @@ -148,7 +148,7 @@ def _get_symbol_signature(node: chapel.AstNode) -> List[Component]:
return [_wrap_str(node.name())]


def _node_to_string(node: chapel.AstNode) -> List[Component]:
def _node_to_string(node: chapel.AstNode, sep="") -> List[Component]:
"""
General helper method to convert an AstNode to a string representation. If
it doesn't know how to convert the node, it returns "<...>"
Expand All @@ -157,6 +157,11 @@ def _node_to_string(node: chapel.AstNode) -> List[Component]:
return _get_symbol_signature(node)
elif isinstance(node, chapel.Identifier):
return [_wrap_str(node.name())]
elif isinstance(node, chapel.Dot):
return _node_to_string(node.receiver()) + [
_wrap_str("."),
_wrap_str(node.field()),
]
elif isinstance(node, chapel.BoolLiteral):
return [_wrap_str("true" if node.value() else "false")]
elif isinstance(
Expand All @@ -175,6 +180,17 @@ def _node_to_string(node: chapel.AstNode) -> List[Component]:
return [_wrap_str('c"' + node.value() + '"')]
elif isinstance(node, chapel.FnCall):
return _fncall_to_string(node)
elif isinstance(node, chapel.OpCall):
return _opcall_to_string(node)
elif isinstance(node, chapel.IndexableLoop):
return _indexable_loop_to_string(node)
elif isinstance(node, chapel.Domain):
return _domain_to_string(node)
elif isinstance(node, chapel.Range):
return _range_to_string(node)
elif isinstance(node, chapel.Block):
return _list_to_string(node.stmts(), sep)

return [Component(ComponentTag.PLACEHOLDER, None)]


Expand Down Expand Up @@ -329,3 +345,118 @@ def _fncall_to_string(call: chapel.FnCall) -> List[Component]:
comps.append(_wrap_str(closebr))

return comps


def _opcall_to_string(call: chapel.OpCall) -> List[Component]:
"""
Convert a call to a string
"""

def op_to_string(op: str) -> str:
special = {"#": "#", ":": ": "}
return special.get(op, f" {op} ")

comps = []
paren = call.parenth_location()
if paren:
comps.append(_wrap_str("("))
if call.is_unary_op():
comps.append(_wrap_str(call.op()))
comps.extend(_node_to_string(call.actual(0)))
else:
comps.extend(_node_to_string(call.actual(0)))
comps.append(_wrap_str(op_to_string(call.op())))
comps.extend(_node_to_string(call.actual(1)))
if paren:
comps.append(_wrap_str(")"))
return comps


def _range_to_string(range: chapel.Range) -> List[Component]:
"""
Convert a range to a string
"""
comps = []
low = range.lower_bound()
if low:
comps.extend(_node_to_string(low))
comps.append(_wrap_str(range.op_kind()))
high = range.upper_bound()
if high:
comps.extend(_node_to_string(high))
return comps


def _domain_to_string(domain: chapel.Domain) -> List[Component]:
"""
Convert a domain to a string
"""
comps = []
if domain.used_curly_braces():
comps.append(_wrap_str("{"))
do_comma = False
for e in domain.exprs():
if do_comma:
comps.append(_wrap_str(", "))
do_comma = True
comps.extend(_node_to_string(e))
if domain.used_curly_braces():
comps.append(_wrap_str("}"))
return comps


def _list_to_string(
elms: Iterable[chapel.AstNode], sep=None, prefix=None, postfix=None
) -> List[Component]:
"""
Convert a list of nodes to a string
"""
comps = []
if prefix:
comps.append(_wrap_str(prefix))
do_sep = False
for e in elms:
if sep and do_sep:
comps.append(_wrap_str(sep))
do_sep = True
comps.extend(_node_to_string(e))
if postfix:
comps.append(_wrap_str(postfix))
return comps


def _indexable_loop_to_string(loop: chapel.IndexableLoop) -> List[Component]:
"""
Convert an indexable loop to a string
"""

if not loop.is_expression_level():
# we only support expression-level loops for now
return [Component(ComponentTag.PLACEHOLDER, None)]

parts = {
chapel.BracketLoop: ("[", "]", " "),
chapel.For: ("for ", "", " do "),
}
part = parts.get(type(loop))
if part is None:
return [Component(ComponentTag.PLACEHOLDER, None)]

comps = []
comps.append(_wrap_str(part[0]))
idx = loop.index()
if idx:
comps.extend(_node_to_string(idx))
comps.append(_wrap_str(" in "))
comps.extend(_node_to_string(loop.iterand()))
with_ = loop.with_clause()
if with_:
comps.append(_wrap_str(" with "))
comps.extend(_node_to_string(with_))

comps.append(_wrap_str(part[1]))

comps.append(_wrap_str(part[2]))
comps.extend(_node_to_string(loop.body()))

return comps
33 changes: 33 additions & 0 deletions tools/chpl-language-server/test/document_symbols.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,36 @@ async def test_document_symbols_nested(client: LanguageClient):

async with source_file(client, file) as doc:
await check_symbol_information(client, doc, symbols)


@pytest.mark.asyncio
async def test_document_symbols_exprs(client: LanguageClient):
"""
Test that document symbols for more complex expressions are correct
This test ensures that we can round-trip well formed expressions through
'symbol_signature'. 'symbol_signature' will auto correct spacing and remove
inline comments, which is not tested here.
Note: const is erroneously not included in the location
"""

exprs = [
"const a = 10 + 10;",
"const b: [1..10] int = 2 * ((-3) + 1i);",
"const c = [{1..10 by 2}] 1;",
"const d = 2..#5;",
"const e = for 1..10 do 1;",
"const f = {(1 + 2)..<10};",
"const g = -f.size;",
]
file = "\n".join(exprs)

symbols = []
for i, e in enumerate(exprs):
exp_str = e.removesuffix(";")
exp_sym = (rng((i, 6), (i, len(exp_str))), exp_str, SymbolKind.Constant)
symbols.append(exp_sym)

async with source_file(client, file) as doc:
await check_symbol_information(client, doc, symbols)

0 comments on commit 0700b68

Please sign in to comment.