feat(inlay-hints): add configurable parameter name hints
Adds configurable inlay hints for TCL procedures and merges signatures from built-ins and workspace files. The feature supports parameterNames and suppressWhenArgumentMatchesName and respects an optional range filter and current-file priority. - Introduces built-in and custom inlay hint builders - Honors inlayHints parameterNames and suppression options - Adds tests validating hints, ranges, and priority rules
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
THIS_DIR = Path(__file__).parent
|
||||
SRC_DIR = THIS_DIR.parent.parent / "src"
|
||||
if str(SRC_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(SRC_DIR))
|
||||
|
||||
import lsprotocol.types as lsp # type: ignore
|
||||
|
||||
from tools.inlay_hint import (
|
||||
InlayHintGenerator,
|
||||
InlayHintParameter,
|
||||
InlayHintSignature,
|
||||
build_builtin_inlay_signatures,
|
||||
build_custom_inlay_signatures,
|
||||
)
|
||||
from tools.navigation import build_file_symbol_index
|
||||
from tools.parser import CustomParser
|
||||
|
||||
|
||||
def _signature(*names: str, variadic: bool = False) -> InlayHintSignature:
|
||||
parameters = tuple(
|
||||
InlayHintParameter(
|
||||
name=name,
|
||||
variadic=variadic and index == len(names) - 1,
|
||||
)
|
||||
for index, name in enumerate(names)
|
||||
)
|
||||
return InlayHintSignature(
|
||||
parameters=parameters,
|
||||
display_label=" ".join(["test_proc", *names]),
|
||||
)
|
||||
|
||||
|
||||
def _generate(
|
||||
source: str,
|
||||
signatures: dict[str, InlayHintSignature],
|
||||
**options,
|
||||
) -> list[lsp.InlayHint]:
|
||||
tree = CustomParser().parse(source)
|
||||
generator = InlayHintGenerator(source, signatures, **options)
|
||||
tree.accept(generator, recurse=True)
|
||||
return generator.hints
|
||||
|
||||
|
||||
def _labels(hints: list[lsp.InlayHint]) -> list[str]:
|
||||
labels = []
|
||||
for hint in hints:
|
||||
if isinstance(hint.label, str):
|
||||
labels.append(hint.label)
|
||||
else:
|
||||
labels.append("".join(part.value for part in hint.label))
|
||||
return labels
|
||||
|
||||
|
||||
def test_many_parameters_are_returned_without_server_side_truncation():
|
||||
names = tuple(f"parameter_{index}" for index in range(10))
|
||||
source = "test_proc " + " ".join(str(index) for index in range(10))
|
||||
|
||||
hints = _generate(source, {"test_proc": _signature(*names)})
|
||||
|
||||
assert _labels(hints) == [f"{name}:" for name in names]
|
||||
assert all("…" not in label and "..." not in label for label in _labels(hints))
|
||||
|
||||
|
||||
def test_only_hints_inside_requested_range_are_returned():
|
||||
source = "test_proc first\nset spacer 1\ntest_proc second"
|
||||
requested_range = lsp.Range(
|
||||
start=lsp.Position(line=2, character=0),
|
||||
end=lsp.Position(line=3, character=0),
|
||||
)
|
||||
|
||||
hints = _generate(
|
||||
source,
|
||||
{"test_proc": _signature("value")},
|
||||
requested_range=requested_range,
|
||||
)
|
||||
|
||||
assert len(hints) == 1
|
||||
assert hints[0].position.line == 2
|
||||
|
||||
|
||||
def test_matching_variable_name_can_be_suppressed():
|
||||
source = "test_proc $value $other"
|
||||
signature = _signature("value", "result")
|
||||
|
||||
suppressed = _generate(source, {"test_proc": signature})
|
||||
visible = _generate(
|
||||
source,
|
||||
{"test_proc": signature},
|
||||
suppress_when_argument_matches_name=False,
|
||||
)
|
||||
|
||||
assert _labels(suppressed) == ["result:"]
|
||||
assert _labels(visible) == ["value:", "result:"]
|
||||
|
||||
|
||||
def test_literal_mode_hides_variable_argument_hints():
|
||||
source = 'test_proc $value "literal"'
|
||||
|
||||
hints = _generate(
|
||||
source,
|
||||
{"test_proc": _signature("first", "second")},
|
||||
parameter_names="literals",
|
||||
)
|
||||
|
||||
assert _labels(hints) == ["second:"]
|
||||
|
||||
|
||||
def test_variadic_parameter_labels_every_remaining_argument():
|
||||
hints = _generate(
|
||||
"test_proc first second third fourth",
|
||||
{"test_proc": _signature("required", "args", variadic=True)},
|
||||
)
|
||||
|
||||
assert _labels(hints) == ["required:", "args:", "args:", "args:"]
|
||||
|
||||
|
||||
def test_builtin_signature_has_variadic_hints_and_parameter_documentation():
|
||||
signatures = build_builtin_inlay_signatures(
|
||||
[
|
||||
{
|
||||
"label": "MOM_force",
|
||||
"description": "Controls address output.",
|
||||
"format": "MOM_force <mode> <address_1 ... address_n>",
|
||||
"parameters": [
|
||||
{"name": "mode", "desc": "Output mode."},
|
||||
{
|
||||
"name": "address_1 ... address_n",
|
||||
"desc": "Output addresses.",
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
hints = _generate("MOM_force Always X Y", signatures)
|
||||
|
||||
assert _labels(hints) == ["mode:", "address:", "address:"]
|
||||
assert hints[1].label[0].tooltip == "Output addresses." # type: ignore[index]
|
||||
assert hints[0].tooltip.value.endswith("Controls address output.") # type: ignore[union-attr]
|
||||
|
||||
|
||||
def test_current_file_signature_and_definition_location_take_priority(tmp_path: Path):
|
||||
other_path = tmp_path / "other.tcl"
|
||||
current_path = tmp_path / "current.tcl"
|
||||
other_source = "proc shared {from_other} { return $from_other }"
|
||||
current_source = "proc shared {from_current args} { return $from_current }"
|
||||
parser = CustomParser()
|
||||
other_tree = parser.parse(other_source)
|
||||
current_tree = parser.parse(current_source)
|
||||
indexes = {
|
||||
str(other_path): build_file_symbol_index(
|
||||
str(other_path), other_path.as_uri(), other_tree
|
||||
),
|
||||
str(current_path): build_file_symbol_index(
|
||||
str(current_path), current_path.as_uri(), current_tree
|
||||
),
|
||||
}
|
||||
|
||||
signatures = build_custom_inlay_signatures(
|
||||
{
|
||||
str(current_path): {"shared": ["from_current", "args"]},
|
||||
str(other_path): {"shared": ["from_other"]},
|
||||
},
|
||||
{
|
||||
str(current_path): {"shared": "Current documentation."},
|
||||
str(other_path): {"shared": "Other documentation."},
|
||||
},
|
||||
indexes,
|
||||
str(current_path),
|
||||
)
|
||||
|
||||
signature = signatures["shared"]
|
||||
assert [parameter.name for parameter in signature.parameters] == [
|
||||
"from_current",
|
||||
"args",
|
||||
]
|
||||
assert signature.parameters[-1].variadic
|
||||
assert signature.documentation == "Current documentation."
|
||||
assert signature.location is not None
|
||||
assert signature.location.uri == current_path.as_uri()
|
||||
|
||||
|
||||
def test_positions_use_lsp_utf16_offsets():
|
||||
source = 'test_proc "😀" second'
|
||||
|
||||
hints = _generate(
|
||||
source,
|
||||
{"test_proc": _signature("first", "second")},
|
||||
)
|
||||
|
||||
assert hints[1].position.character == source.index("second") + 1
|
||||
Reference in New Issue
Block a user