Skip to content

Commit

Permalink
Add completion item details
Browse files Browse the repository at this point in the history
  • Loading branch information
aazuspan committed Aug 11, 2024
1 parent 0e24775 commit ff5d2cb
Show file tree
Hide file tree
Showing 3 changed files with 68 additions and 41 deletions.
78 changes: 42 additions & 36 deletions src/spinasm_lsp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,19 @@ def did_close(
ls.publish_diagnostics(params.text_document.uri, [])


def _get_defined_hover(stxt: str, parser: SPINAsmParser) -> str:
"""Get a hover message with the value of a defined variable or label."""
# Check jmptbl first since labels are also defined in symtbl
if stxt in parser.jmptbl:
hover_definition = parser.jmptbl[stxt]
return f"(label) {stxt}: Offset[{hover_definition}]"
if stxt in parser.symtbl:
hover_definition = parser.symtbl[stxt]
return f"(constant) {stxt}: Literal[{hover_definition}]"

return ""


@server.feature(lsp.TEXT_DOCUMENT_HOVER)
async def hover(ls: SPINAsmLanguageServer, params: lsp.HoverParams) -> lsp.Hover | None:
"""Retrieve documentation from symbols on hover."""
Expand All @@ -100,22 +113,12 @@ async def hover(ls: SPINAsmLanguageServer, params: lsp.HoverParams) -> lsp.Hover
if (token := parser.token_registry.get_token_at_position(params.position)) is None:
return None

token_val = token.symbol["stxt"]
token_type = token.symbol["type"]

hover_msg = None
if token_type in ("LABEL", "TARGET"):
# Label definitions and targets
if token_val in parser.jmptbl:
hover_definition = parser.jmptbl[token_val.upper()]
hover_msg = f"(label) {token_val}: Offset[**{hover_definition}**]"
# Variable and memory definitions
elif token_val in parser.symtbl:
hover_definition = parser.symtbl[token_val.upper()]
hover_msg = f"(constant) {token_val}: Literal[**{hover_definition}**]"
# Opcodes and assignments
elif token_type in ("ASSEMBLER", "MNEMONIC"):
hover_msg = ls.documentation.get(token_val, "")
if token.symbol["type"] in ("LABEL", "TARGET"):
hover_msg = _get_defined_hover(str(token), parser=parser)

elif token.symbol["type"] in ("ASSEMBLER", "MNEMONIC"):
hover_msg = ls.documentation.get(str(token), "")

return (
None
Expand All @@ -133,36 +136,39 @@ async def completions(
"""Returns completion items."""
parser = await ls.get_parser(params.text_document.uri)

opcodes = [k.upper() for k in ls.documentation]
symbols = list(parser.symtbl.keys())
labels = list(parser.jmptbl.keys())
mem = list(parser.mem.keys())
symbol_completions = [
lsp.CompletionItem(
label=symbol,
kind=lsp.CompletionItemKind.Constant,
detail=_get_defined_hover(symbol, parser=parser),
)
for symbol in parser.symtbl
]

opcode_items = [
label_completions = [
lsp.CompletionItem(
label=k,
label=label,
kind=lsp.CompletionItemKind.Reference,
detail=_get_defined_hover(label, parser=parser),
)
for label in parser.jmptbl
]

opcode_completions = [
lsp.CompletionItem(
label=opcode,
kind=lsp.CompletionItemKind.Function,
detail="(opcode)",
documentation=lsp.MarkupContent(
kind=lsp.MarkupKind.Markdown, value=ls.documentation[k.upper()]
kind=lsp.MarkupKind.Markdown, value=ls.documentation[opcode]
),
)
for k in opcodes
]
# TODO: Set details for all the completions below using the same info as the hover.
symbol_items = [
lsp.CompletionItem(label=k, kind=lsp.CompletionItemKind.Constant)
for k in symbols
]
label_items = [
lsp.CompletionItem(label=k, kind=lsp.CompletionItemKind.Reference)
for k in labels
]
mem_items = [
lsp.CompletionItem(label=k, kind=lsp.CompletionItemKind.Variable) for k in mem
for opcode in [k.upper() for k in ls.documentation]
]

return lsp.CompletionList(
is_incomplete=False, items=opcode_items + mem_items + symbol_items + label_items
is_incomplete=False,
items=symbol_completions + label_completions + opcode_completions,
)


Expand Down
14 changes: 10 additions & 4 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class HoverDict(TypedDict):

symbol: str
position: lsp.Position
contains: str
contains: str | None


class PrepareRenameDict(TypedDict):
Expand Down Expand Up @@ -113,17 +113,17 @@ class RenameDict(TypedDict):
{
"symbol": "endclr",
"position": lsp.Position(line=37, character=13),
"contains": "(label) ENDCLR: Offset[**4**]",
"contains": "(label) ENDCLR: Offset[4]",
},
{
"symbol": "mono",
"position": lsp.Position(line=47, character=5),
"contains": "(constant) MONO: Literal[**32**]",
"contains": "(constant) MONO: Literal[32]",
},
{
"symbol": "lap2b#",
"position": lsp.Position(line=73, character=4),
"contains": "(constant) LAP2B#: Literal[**9802**]",
"contains": "(constant) LAP2B#: Literal[9802]",
},
{
# CHO RDA, hovering over CHO
Expand All @@ -137,6 +137,12 @@ class RenameDict(TypedDict):
"position": lsp.Position(line=85, character=4),
"contains": "## `CHO RDA N, C, D`",
},
{
# Hovering over an int, which should return no hover info
"symbol": "None",
"position": lsp.Position(line=8, character=8),
"contains": None,
},
]


Expand Down
17 changes: 16 additions & 1 deletion tests/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ async def test_definition(assignment: dict, client: LanguageClient):

@pytest.mark.asyncio()
async def test_completions(client: LanguageClient):
"""Test that expected completions are shown with details and documentation."""
patch = PATCH_DIR / "Basic.spn"

results = await client.text_document_completion_async(
Expand Down Expand Up @@ -81,6 +82,16 @@ async def test_completions(client: LanguageClient):
for completion in expected_completions:
assert completion in completions, f"Expected completion {completion} not found"

# Completions for defined values should show their literal value
apout_completion = [item for item in results.items if item.label == "APOUT"][0]
assert apout_completion.detail == "(constant) APOUT: Literal[33]"
assert apout_completion.documentation is None

# Completions for opcodes should include their documentation
cho_rda_completion = [item for item in results.items if item.label == "CHO RDA"][0]
assert cho_rda_completion.detail == "(opcode)"
assert "## `CHO RDA N, C, D`" in str(cho_rda_completion.documentation)


@pytest.mark.asyncio()
async def test_diagnostic_parsing_errors(client: LanguageClient):
Expand Down Expand Up @@ -161,7 +172,11 @@ async def test_hover(hover: dict, client: LanguageClient):
)
)

assert hover["contains"] in result.contents.value
if hover["contains"] is None:
assert result is None, "Expected no hover result"
else:
msg = f"Hover does not contain `{hover['contains']}`"
assert hover["contains"] in result.contents.value, msg


@pytest.mark.parametrize("prepare", PREPARE_RENAMES, ids=lambda x: x["symbol"])
Expand Down

0 comments on commit ff5d2cb

Please sign in to comment.