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:
@@ -1,5 +1,8 @@
|
|||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
- Prevent truncated TCL inlay hints and add configurable parameter hint modes
|
||||||
|
- Add inlay hints for built-in NX procedures, variadic arguments, and visible ranges
|
||||||
|
- Add inlay hint documentation and navigation to custom procedure definitions
|
||||||
- Add signature help for custom TCL procedures and built-in NX/MOM procedures
|
- Add signature help for custom TCL procedures and built-in NX/MOM procedures
|
||||||
- Clean stale TCL indexes on close, delete, and rename operations
|
- Clean stale TCL indexes on close, delete, and rename operations
|
||||||
- Make background parsing and index updates thread-safe
|
- Make background parsing and index updates thread-safe
|
||||||
|
|||||||
@@ -31,6 +31,12 @@ The extension can be configured through VS Code settings:
|
|||||||
- `nx-post-support.interpreter` - Specify custom Python interpreter path for the language server
|
- `nx-post-support.interpreter` - Specify custom Python interpreter path for the language server
|
||||||
- `nx-post-support.formatter` - Enable/disable the TCL formatter (default: false)
|
- `nx-post-support.formatter` - Enable/disable the TCL formatter (default: false)
|
||||||
- `nx-post-support.inlayHint` - Enable/disable inlay Hints (default: true)
|
- `nx-post-support.inlayHint` - Enable/disable inlay Hints (default: true)
|
||||||
|
- `nx-post-support.inlayHints.parameterNames` - Show parameter names for `all`, only `literals`, or `none` (default: `all`)
|
||||||
|
- `nx-post-support.inlayHints.suppressWhenArgumentMatchesName` - Hide redundant hints such as `value:` before `$value` (default: true)
|
||||||
|
|
||||||
|
TCL files default to unlimited inlay hint length so that VS Code does not
|
||||||
|
truncate later parameter names on a line. An explicit user setting for
|
||||||
|
`editor.inlayHints.maximumLength` still takes precedence.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,12 @@ export interface ISettings {
|
|||||||
interpreter: string[]
|
interpreter: string[]
|
||||||
importStrategy: string
|
importStrategy: string
|
||||||
showNotifications: string
|
showNotifications: string
|
||||||
|
formatter: boolean
|
||||||
|
inlayHint: boolean
|
||||||
|
inlayHints: {
|
||||||
|
parameterNames: "all" | "literals" | "none"
|
||||||
|
suppressWhenArgumentMatchesName: boolean
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getExtensionSettings(
|
export function getExtensionSettings(
|
||||||
@@ -80,7 +86,13 @@ export async function getWorkspaceSettings(
|
|||||||
importStrategy: config.get<string>(`importStrategy`) ?? "useBundled",
|
importStrategy: config.get<string>(`importStrategy`) ?? "useBundled",
|
||||||
showNotifications: config.get<string>(`showNotifications`) ?? "off",
|
showNotifications: config.get<string>(`showNotifications`) ?? "off",
|
||||||
formatter: config.get<boolean>(`formatter`) ?? true,
|
formatter: config.get<boolean>(`formatter`) ?? true,
|
||||||
inlayHint: config.get<boolean>(`inlayHint`) ?? true
|
inlayHint: config.get<boolean>(`inlayHint`) ?? true,
|
||||||
|
inlayHints: {
|
||||||
|
parameterNames:
|
||||||
|
config.get<"all" | "literals" | "none">(`inlayHints.parameterNames`) ?? "all",
|
||||||
|
suppressWhenArgumentMatchesName:
|
||||||
|
config.get<boolean>(`inlayHints.suppressWhenArgumentMatchesName`) ?? true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return workspaceSetting
|
return workspaceSetting
|
||||||
}
|
}
|
||||||
@@ -113,7 +125,13 @@ export async function getGlobalSettings(
|
|||||||
importStrategy: getGlobalValue<string>(config, "importStrategy", "useBundled"),
|
importStrategy: getGlobalValue<string>(config, "importStrategy", "useBundled"),
|
||||||
showNotifications: getGlobalValue<string>(config, "showNotifications", "off"),
|
showNotifications: getGlobalValue<string>(config, "showNotifications", "off"),
|
||||||
formatter: config.get<boolean>(`formatter`) ?? true,
|
formatter: config.get<boolean>(`formatter`) ?? true,
|
||||||
inlayHint: config.get<boolean>(`inlayHint`) ?? true
|
inlayHint: config.get<boolean>(`inlayHint`) ?? true,
|
||||||
|
inlayHints: {
|
||||||
|
parameterNames:
|
||||||
|
config.get<"all" | "literals" | "none">(`inlayHints.parameterNames`) ?? "all",
|
||||||
|
suppressWhenArgumentMatchesName:
|
||||||
|
config.get<boolean>(`inlayHints.suppressWhenArgumentMatchesName`) ?? true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return setting
|
return setting
|
||||||
}
|
}
|
||||||
@@ -129,7 +147,9 @@ export function checkIfConfigurationChanged(
|
|||||||
`${namespace}.importStrategy`,
|
`${namespace}.importStrategy`,
|
||||||
`${namespace}.showNotifications`,
|
`${namespace}.showNotifications`,
|
||||||
`${namespace}.formatter`,
|
`${namespace}.formatter`,
|
||||||
`${namespace}.inlayHint`
|
`${namespace}.inlayHint`,
|
||||||
|
`${namespace}.inlayHints.parameterNames`,
|
||||||
|
`${namespace}.inlayHints.suppressWhenArgumentMatchesName`
|
||||||
]
|
]
|
||||||
const changed = settings.map((s) => e.affectsConfiguration(s))
|
const changed = settings.map((s) => e.affectsConfiguration(s))
|
||||||
return changed.includes(true)
|
return changed.includes(true)
|
||||||
|
|||||||
@@ -96,6 +96,26 @@
|
|||||||
"default": true,
|
"default": true,
|
||||||
"description": "Use the Inlay Hints in from `NX Postprocessor Support`"
|
"description": "Use the Inlay Hints in from `NX Postprocessor Support`"
|
||||||
},
|
},
|
||||||
|
"nx-post-support.inlayHints.parameterNames": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "all",
|
||||||
|
"enum": [
|
||||||
|
"all",
|
||||||
|
"literals",
|
||||||
|
"none"
|
||||||
|
],
|
||||||
|
"enumDescriptions": [
|
||||||
|
"Show parameter name hints for all arguments.",
|
||||||
|
"Show parameter name hints only for literal arguments.",
|
||||||
|
"Do not show parameter name hints."
|
||||||
|
],
|
||||||
|
"description": "Controls which TCL procedure arguments receive parameter name hints."
|
||||||
|
},
|
||||||
|
"nx-post-support.inlayHints.suppressWhenArgumentMatchesName": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"description": "Hide a parameter hint when a variable argument already has the same name, for example `output` in `my_proc $output`."
|
||||||
|
},
|
||||||
"nx-post-support.importStrategy": {
|
"nx-post-support.importStrategy": {
|
||||||
"default": "useBundled",
|
"default": "useBundled",
|
||||||
"description": "Defines where `NX Postprocessor Support` is imported from.",
|
"description": "Defines where `NX Postprocessor Support` is imported from.",
|
||||||
@@ -120,6 +140,11 @@
|
|||||||
"type": "array"
|
"type": "array"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"configurationDefaults": {
|
||||||
|
"[tcl]": {
|
||||||
|
"editor.inlayHints.maximumLength": 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -44,7 +44,11 @@ from pygls import uris, workspace
|
|||||||
from common.load_data import standard_items
|
from common.load_data import standard_items
|
||||||
from tools.folding_ranges import build_folding_ranges
|
from tools.folding_ranges import build_folding_ranges
|
||||||
from tools.semantic_tokens import _Highlighter, TOKEN_TYPES, TokenModifier
|
from tools.semantic_tokens import _Highlighter, TOKEN_TYPES, TokenModifier
|
||||||
from tools.inlay_hint import InlayHintGenerator
|
from tools.inlay_hint import (
|
||||||
|
InlayHintGenerator,
|
||||||
|
build_builtin_inlay_signatures,
|
||||||
|
build_custom_inlay_signatures,
|
||||||
|
)
|
||||||
from tools.navigation import (
|
from tools.navigation import (
|
||||||
SymbolIdentity,
|
SymbolIdentity,
|
||||||
definition_identities,
|
definition_identities,
|
||||||
@@ -333,20 +337,43 @@ def document_symbols(params: lsp.DocumentSymbolParams):
|
|||||||
|
|
||||||
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_INLAY_HINT)
|
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_INLAY_HINT)
|
||||||
def inlay_hints(params: lsp.InlayHintParams):
|
def inlay_hints(params: lsp.InlayHintParams):
|
||||||
if not GLOBAL_SETTINGS.get("inlayHint", False):
|
|
||||||
return []
|
|
||||||
document = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
|
document = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
|
||||||
|
settings = _get_settings_by_document(document)
|
||||||
|
if not settings.get("inlayHint", False):
|
||||||
|
return []
|
||||||
|
|
||||||
|
inlay_settings = settings.get("inlayHints", {})
|
||||||
|
parameter_names = inlay_settings.get("parameterNames", "all")
|
||||||
|
if parameter_names == "none":
|
||||||
|
return []
|
||||||
|
|
||||||
# Reuse cached AST
|
# Reuse cached AST
|
||||||
tree = LSP_SERVER.get_tree(document)
|
tree = LSP_SERVER.get_tree(document)
|
||||||
|
|
||||||
# Merge proc signatures across files and traverse once
|
# Built-in NX procedures are the fallback. Workspace procedures replace them,
|
||||||
merged_signatures = {}
|
# and a declaration in the current file wins over duplicate workspace names.
|
||||||
_, proc_signatures, _ = LSP_SERVER.index_snapshot()
|
signatures = build_builtin_inlay_signatures(
|
||||||
for sigs in proc_signatures.values():
|
standard_items.json_data.get("MOM_procs", [])
|
||||||
merged_signatures.update(sigs)
|
)
|
||||||
|
_, proc_signatures, proc_docs = LSP_SERVER.index_snapshot()
|
||||||
|
signatures.update(
|
||||||
|
build_custom_inlay_signatures(
|
||||||
|
proc_signatures,
|
||||||
|
proc_docs,
|
||||||
|
LSP_SERVER.navigation_snapshot(),
|
||||||
|
document.path,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
generator = InlayHintGenerator(merged_signatures)
|
generator = InlayHintGenerator(
|
||||||
|
document.source,
|
||||||
|
signatures,
|
||||||
|
requested_range=params.range,
|
||||||
|
parameter_names=parameter_names,
|
||||||
|
suppress_when_argument_matches_name=inlay_settings.get(
|
||||||
|
"suppressWhenArgumentMatchesName", True
|
||||||
|
),
|
||||||
|
)
|
||||||
tree.accept(generator, recurse=True)
|
tree.accept(generator, recurse=True)
|
||||||
return generator.hints
|
return generator.hints
|
||||||
|
|
||||||
@@ -769,6 +796,13 @@ def _get_global_defaults():
|
|||||||
"showNotifications": GLOBAL_SETTINGS.get("showNotifications", "off"),
|
"showNotifications": GLOBAL_SETTINGS.get("showNotifications", "off"),
|
||||||
"formatter": GLOBAL_SETTINGS.get("formatter", True),
|
"formatter": GLOBAL_SETTINGS.get("formatter", True),
|
||||||
"inlayHint": GLOBAL_SETTINGS.get("inlayHint", True),
|
"inlayHint": GLOBAL_SETTINGS.get("inlayHint", True),
|
||||||
|
"inlayHints": GLOBAL_SETTINGS.get(
|
||||||
|
"inlayHints",
|
||||||
|
{
|
||||||
|
"parameterNames": "all",
|
||||||
|
"suppressWhenArgumentMatchesName": True,
|
||||||
|
},
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+216
-16
@@ -1,29 +1,229 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import lsprotocol.types as lsp
|
import lsprotocol.types as lsp
|
||||||
from tclint.syntax_tree import Visitor, Command
|
from tclint.syntax_tree import Command, VarSub, Visitor
|
||||||
|
|
||||||
|
from tools.navigation import FileSymbolIndex
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class InlayHintParameter:
|
||||||
|
name: str
|
||||||
|
documentation: str | None = None
|
||||||
|
variadic: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class InlayHintSignature:
|
||||||
|
parameters: tuple[InlayHintParameter, ...]
|
||||||
|
display_label: str
|
||||||
|
documentation: str | None = None
|
||||||
|
location: lsp.Location | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def _normalized_path(path: str) -> str:
|
||||||
|
return os.path.normcase(os.path.abspath(path))
|
||||||
|
|
||||||
|
|
||||||
|
def _definition_location(
|
||||||
|
index: FileSymbolIndex | None, proc_name: str
|
||||||
|
) -> lsp.Location | None:
|
||||||
|
if index is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
basename = proc_name.removeprefix("::").rsplit("::", 1)[-1]
|
||||||
|
for occurrence in reversed(index.occurrences):
|
||||||
|
if (
|
||||||
|
occurrence.is_definition
|
||||||
|
and occurrence.identity.kind == "proc"
|
||||||
|
and occurrence.placeholder == basename
|
||||||
|
):
|
||||||
|
return lsp.Location(uri=index.uri, range=occurrence.range)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def build_custom_inlay_signatures(
|
||||||
|
signatures_by_path: dict[str, dict[str, list[str]]],
|
||||||
|
docs_by_path: dict[str, dict[str, str]],
|
||||||
|
indexes_by_path: dict[str, FileSymbolIndex],
|
||||||
|
current_path: str,
|
||||||
|
) -> dict[str, InlayHintSignature]:
|
||||||
|
"""Merge workspace signatures deterministically, preferring the current file."""
|
||||||
|
current_normalized = _normalized_path(current_path)
|
||||||
|
paths = sorted(
|
||||||
|
signatures_by_path, key=lambda path: _normalized_path(path).casefold()
|
||||||
|
)
|
||||||
|
paths.sort(key=lambda path: _normalized_path(path) == current_normalized)
|
||||||
|
|
||||||
|
result: dict[str, InlayHintSignature] = {}
|
||||||
|
for path in paths:
|
||||||
|
docs = docs_by_path.get(path, {})
|
||||||
|
index = indexes_by_path.get(path)
|
||||||
|
for proc_name, parameter_names in signatures_by_path[path].items():
|
||||||
|
parameters = tuple(
|
||||||
|
InlayHintParameter(
|
||||||
|
name=parameter_name,
|
||||||
|
variadic=(
|
||||||
|
parameter_name == "args"
|
||||||
|
and parameter_index == len(parameter_names) - 1
|
||||||
|
),
|
||||||
|
)
|
||||||
|
for parameter_index, parameter_name in enumerate(parameter_names)
|
||||||
|
)
|
||||||
|
result[proc_name] = InlayHintSignature(
|
||||||
|
parameters=parameters,
|
||||||
|
display_label=" ".join([proc_name, *parameter_names]),
|
||||||
|
documentation=docs.get(proc_name),
|
||||||
|
location=_definition_location(index, proc_name),
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _is_builtin_variadic(item: dict[str, Any], parameter_name: str, index: int) -> bool:
|
||||||
|
parameters = item.get("parameters", [])
|
||||||
|
if index != len(parameters) - 1:
|
||||||
|
return False
|
||||||
|
if "..." in parameter_name or "…" in parameter_name:
|
||||||
|
return True
|
||||||
|
|
||||||
|
format_label = item.get("format", "")
|
||||||
|
return (
|
||||||
|
f"<{parameter_name}>+" in format_label or f"[{parameter_name}]+" in format_label
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _builtin_parameter_label(parameter_name: str) -> str:
|
||||||
|
if "..." not in parameter_name and "…" not in parameter_name:
|
||||||
|
return parameter_name.strip("<>[]")
|
||||||
|
|
||||||
|
first_name = parameter_name.split()[0].strip("<>[]")
|
||||||
|
return re.sub(r"(?:_?1)$", "", first_name) or first_name
|
||||||
|
|
||||||
|
|
||||||
|
def build_builtin_inlay_signatures(
|
||||||
|
items: list[dict[str, Any]],
|
||||||
|
) -> dict[str, InlayHintSignature]:
|
||||||
|
result = {}
|
||||||
|
for item in items:
|
||||||
|
proc_name = item.get("label")
|
||||||
|
if not proc_name:
|
||||||
|
continue
|
||||||
|
parameters = tuple(
|
||||||
|
InlayHintParameter(
|
||||||
|
name=_builtin_parameter_label(parameter.get("name", "")),
|
||||||
|
documentation=parameter.get("desc") or None,
|
||||||
|
variadic=_is_builtin_variadic(
|
||||||
|
item, parameter.get("name", ""), parameter_index
|
||||||
|
),
|
||||||
|
)
|
||||||
|
for parameter_index, parameter in enumerate(item.get("parameters", []))
|
||||||
|
if parameter.get("name")
|
||||||
|
)
|
||||||
|
result[proc_name] = InlayHintSignature(
|
||||||
|
parameters=parameters,
|
||||||
|
display_label=item.get("format") or proc_name,
|
||||||
|
documentation=item.get("description") or None,
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _position_in_range(
|
||||||
|
position: lsp.Position, requested_range: lsp.Range | None
|
||||||
|
) -> bool:
|
||||||
|
if requested_range is None:
|
||||||
|
return True
|
||||||
|
value = (position.line, position.character)
|
||||||
|
start = (requested_range.start.line, requested_range.start.character)
|
||||||
|
end = (requested_range.end.line, requested_range.end.character)
|
||||||
|
return start <= value < end
|
||||||
|
|
||||||
|
|
||||||
class InlayHintGenerator(Visitor):
|
class InlayHintGenerator(Visitor):
|
||||||
def __init__(self, proc_signatures):
|
def __init__(
|
||||||
|
self,
|
||||||
|
source: str,
|
||||||
|
proc_signatures: dict[str, InlayHintSignature],
|
||||||
|
*,
|
||||||
|
requested_range: lsp.Range | None = None,
|
||||||
|
parameter_names: str = "all",
|
||||||
|
suppress_when_argument_matches_name: bool = True,
|
||||||
|
):
|
||||||
|
self.source_lines = source.splitlines()
|
||||||
self.proc_signatures = proc_signatures
|
self.proc_signatures = proc_signatures
|
||||||
self.hints = []
|
self.requested_range = requested_range
|
||||||
|
self.parameter_names = parameter_names
|
||||||
|
self.suppress_when_argument_matches_name = suppress_when_argument_matches_name
|
||||||
|
self.hints: list[lsp.InlayHint] = []
|
||||||
|
|
||||||
|
def _position(self, line: int, column: int) -> lsp.Position:
|
||||||
|
line_index = line - 1
|
||||||
|
character_index = column - 1
|
||||||
|
if 0 <= line_index < len(self.source_lines):
|
||||||
|
prefix = self.source_lines[line_index][:character_index]
|
||||||
|
character_index = len(prefix.encode("utf-16-le")) // 2
|
||||||
|
return lsp.Position(line=line_index, character=character_index)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _parameter_for_argument(
|
||||||
|
signature: InlayHintSignature, argument_index: int
|
||||||
|
) -> InlayHintParameter | None:
|
||||||
|
if argument_index < len(signature.parameters):
|
||||||
|
return signature.parameters[argument_index]
|
||||||
|
if signature.parameters and signature.parameters[-1].variadic:
|
||||||
|
return signature.parameters[-1]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _should_show(self, argument, parameter: InlayHintParameter) -> bool:
|
||||||
|
if self.parameter_names == "none":
|
||||||
|
return False
|
||||||
|
if self.parameter_names == "literals" and isinstance(argument, VarSub):
|
||||||
|
return False
|
||||||
|
if not self.suppress_when_argument_matches_name or not isinstance(
|
||||||
|
argument, VarSub
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
return getattr(argument, "value", None) != parameter.name
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _tooltip(signature: InlayHintSignature) -> lsp.MarkupContent:
|
||||||
|
value = f"`{signature.display_label}`"
|
||||||
|
if signature.documentation:
|
||||||
|
value += f"\n\n{signature.documentation}"
|
||||||
|
return lsp.MarkupContent(kind=lsp.MarkupKind.Markdown, value=value)
|
||||||
|
|
||||||
def visit_command(self, command: Command):
|
def visit_command(self, command: Command):
|
||||||
name = getattr(command.routine, "contents", None)
|
name = getattr(command.routine, "contents", None)
|
||||||
if name not in self.proc_signatures:
|
signature = self.proc_signatures.get(name)
|
||||||
|
if signature is None or self.parameter_names == "none":
|
||||||
return
|
return
|
||||||
|
|
||||||
param_names = self.proc_signatures[name]
|
for argument_index, argument in enumerate(command.args):
|
||||||
for idx, arg in enumerate(command.args):
|
parameter = self._parameter_for_argument(signature, argument_index)
|
||||||
if idx >= len(param_names):
|
if parameter is None:
|
||||||
break
|
break
|
||||||
param_name = param_names[idx]
|
if not argument.pos or not self._should_show(argument, parameter):
|
||||||
|
continue
|
||||||
|
|
||||||
if arg.pos:
|
line, column = argument.pos
|
||||||
line, col = arg.pos
|
position = self._position(line, column)
|
||||||
self.hints.append(
|
if not _position_in_range(position, self.requested_range):
|
||||||
lsp.InlayHint(
|
continue
|
||||||
position=lsp.Position(line=line - 1, character=col - 1),
|
|
||||||
label=f"{param_name}:",
|
label = lsp.InlayHintLabelPart(
|
||||||
kind=lsp.InlayHintKind.Parameter,
|
value=f"{parameter.name}:",
|
||||||
)
|
tooltip=parameter.documentation,
|
||||||
|
location=signature.location,
|
||||||
|
)
|
||||||
|
self.hints.append(
|
||||||
|
lsp.InlayHint(
|
||||||
|
position=position,
|
||||||
|
label=[label],
|
||||||
|
kind=lsp.InlayHintKind.Parameter,
|
||||||
|
tooltip=self._tooltip(signature),
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|||||||
@@ -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