Merge pull request 'Recognize derived .def names and show .def previews on completion resolve' (#48) from enhancements into main
This commit is contained in:
@@ -0,0 +1,39 @@
|
|||||||
|
name: Tests
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
jobs:
|
||||||
|
python:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
- name: Install pytest
|
||||||
|
run: pip install pytest
|
||||||
|
- name: Run language server tests
|
||||||
|
working-directory: server
|
||||||
|
# The server's dependencies are bundled in server/libs.
|
||||||
|
env:
|
||||||
|
PYTHONPATH: libs
|
||||||
|
run: python -m pytest tests/python_tests -q
|
||||||
|
node:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install NodeJS
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
- name: Install NPM Packages
|
||||||
|
run: |
|
||||||
|
npm ci
|
||||||
|
cd ./client
|
||||||
|
npm ci
|
||||||
|
- name: Run extension tests
|
||||||
|
run: node --test test/
|
||||||
|
- name: Build extension
|
||||||
|
run: npm run package
|
||||||
@@ -11,6 +11,8 @@ Versions correspond to the Git tags of this repository.
|
|||||||
- Hover over block templates shows the template body; hover over addresses shows format, leader, trailer, min/max, and modality
|
- Hover over block templates shows the template body; hover over addresses shows format, leader, trailer, min/max, and modality
|
||||||
- Find References and Rename for block templates and addresses across Tcl and `.def` files, including addresses used inside block templates
|
- Find References and Rename for block templates and addresses across Tcl and `.def` files, including addresses used inside block templates
|
||||||
- Go to Definition, hover, references, and rename also work inside `.def` files
|
- Go to Definition, hover, references, and rename also work inside `.def` files
|
||||||
|
- Hover and Go to Definition also recognize block template and address names that reach an NX command through a local variable (`set`, `lappend`, `list`, `foreach`) or through the parameter of a custom proc such as `LIB_SPF_call_cycle "absolute_mode"`, including nested wrapper procs; same-named strings without such a path are not recognized, and these derived names are not renamed
|
||||||
|
- Completion items for block templates and addresses (`BLOCK_LIST`, `ADDR_LIST`, `MOM_do_template`, ...) show the same preview as the hover
|
||||||
|
|
||||||
### Documentation
|
### Documentation
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ from tools.def_navigation import (
|
|||||||
def_rename_edits,
|
def_rename_edits,
|
||||||
def_symbol_at,
|
def_symbol_at,
|
||||||
)
|
)
|
||||||
|
from tools.def_flow import derived_def_symbol
|
||||||
from tools.def_symbols import parse_def_document
|
from tools.def_symbols import parse_def_document
|
||||||
from tools.folding_ranges import build_folding_ranges
|
from tools.folding_ranges import build_folding_ranges
|
||||||
from tools.index_cache import IndexCache
|
from tools.index_cache import IndexCache
|
||||||
@@ -287,7 +288,7 @@ def document_diagnostic(params: lsp.DocumentDiagnosticParams):
|
|||||||
|
|
||||||
@LSP_SERVER.feature(
|
@LSP_SERVER.feature(
|
||||||
lsp.TEXT_DOCUMENT_COMPLETION,
|
lsp.TEXT_DOCUMENT_COMPLETION,
|
||||||
lsp.CompletionOptions(trigger_characters=["$", " ", "-", "(", ","]),
|
lsp.CompletionOptions(trigger_characters=["$", " ", "-", "(", ","], resolve_provider=True),
|
||||||
)
|
)
|
||||||
def on_completion(params: lsp.CompletionParams) -> lsp.CompletionList:
|
def on_completion(params: lsp.CompletionParams) -> lsp.CompletionList:
|
||||||
result = _on_completion(params)
|
result = _on_completion(params)
|
||||||
@@ -300,6 +301,17 @@ def on_completion(params: lsp.CompletionParams) -> lsp.CompletionList:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@LSP_SERVER.feature(lsp.COMPLETION_ITEM_RESOLVE)
|
||||||
|
def on_completion_resolve(item: lsp.CompletionItem) -> lsp.CompletionItem:
|
||||||
|
"""Show the .def declaration of a selected block template/address, as on hover."""
|
||||||
|
target = item.data.get("def") if isinstance(item.data, dict) else None
|
||||||
|
if item.documentation is None and isinstance(target, list) and len(target) == 2:
|
||||||
|
markdown = def_hover_markdown(LSP_SERVER.def_documents_snapshot(), tuple(target))
|
||||||
|
if markdown:
|
||||||
|
item.documentation = lsp.MarkupContent(kind=lsp.MarkupKind.Markdown, value=markdown)
|
||||||
|
return item
|
||||||
|
|
||||||
|
|
||||||
# Keywords that expand to all loaded .def names: keyword -> (items, description).
|
# Keywords that expand to all loaded .def names: keyword -> (items, description).
|
||||||
SYMBOL_LIST_KEYWORDS = {
|
SYMBOL_LIST_KEYWORDS = {
|
||||||
"BLOCK_LIST": (lambda: LSP_SERVER.block_template_items(), "block templates"),
|
"BLOCK_LIST": (lambda: LSP_SERVER.block_template_items(), "block templates"),
|
||||||
@@ -767,16 +779,53 @@ def _def_target(identity: SymbolIdentity) -> tuple[str, str] | None:
|
|||||||
return (identity.kind, identity.name) if identity.kind in DEF_SYMBOL_KINDS else None
|
return (identity.kind, identity.name) if identity.kind in DEF_SYMBOL_KINDS else None
|
||||||
|
|
||||||
|
|
||||||
def _tcl_def_hover(uri: str, position: lsp.Position, token: str) -> lsp.Hover | None:
|
def _word_at(uri: str, position: lsp.Position) -> str | None:
|
||||||
|
document = LSP_SERVER.workspace.get_text_document(uri)
|
||||||
|
try:
|
||||||
|
line = LSP_SERVER.get_lines(document)[position.line]
|
||||||
|
except IndexError:
|
||||||
|
return None
|
||||||
|
for match in re.finditer(r"\b\w+\b", line):
|
||||||
|
if match.start() <= position.character <= match.end():
|
||||||
|
return match.group(0)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _tcl_def_symbol(uri: str, position: lsp.Position, token: str | None):
|
||||||
|
"""The .def target at ``position``: a direct NX command argument or a derived name.
|
||||||
|
|
||||||
|
Returns (documents, target, range) or None.
|
||||||
|
"""
|
||||||
documents = LSP_SERVER.def_documents_snapshot()
|
documents = LSP_SERVER.def_documents_snapshot()
|
||||||
if not any(token in document.names(kind) for document in documents.values() for kind in DEF_SYMBOL_KINDS):
|
# Cheap guard: only names declared in a .def file are analyzed at all.
|
||||||
|
kinds = {kind for kind in DEF_SYMBOL_KINDS if token and any(token in document.names(kind) for document in documents.values())}
|
||||||
|
if not kinds:
|
||||||
return None
|
return None
|
||||||
context = _navigation_context(uri, position)
|
context = _navigation_context(uri, position)
|
||||||
target = context and _def_target(context[3])
|
target = context and _def_target(context[3])
|
||||||
markdown = target and def_hover_markdown(documents, target)
|
if target:
|
||||||
|
return documents, target, context[2].range
|
||||||
|
try:
|
||||||
|
tree = LSP_SERVER.get_tree(LSP_SERVER.workspace.get_text_document(uri))
|
||||||
|
except TclSyntaxError:
|
||||||
|
return None
|
||||||
|
derived = derived_def_symbol(tree, position, LSP_SERVER.def_wrapper_table())
|
||||||
|
if derived is None or derived[1] != token:
|
||||||
|
return None
|
||||||
|
found, name, range_ = derived
|
||||||
|
kind = next((kind for kind in sorted(found & kinds) if def_declarations(documents, (kind, name))), None)
|
||||||
|
return (documents, (kind, name), range_) if kind else None
|
||||||
|
|
||||||
|
|
||||||
|
def _tcl_def_hover(uri: str, position: lsp.Position, token: str) -> lsp.Hover | None:
|
||||||
|
symbol = _tcl_def_symbol(uri, position, token)
|
||||||
|
if symbol is None:
|
||||||
|
return None
|
||||||
|
documents, target, range_ = symbol
|
||||||
|
markdown = def_hover_markdown(documents, target)
|
||||||
if not markdown:
|
if not markdown:
|
||||||
return None
|
return None
|
||||||
return lsp.Hover(lsp.MarkupContent(kind=lsp.MarkupKind.Markdown, value=markdown), range=context[2].range)
|
return lsp.Hover(lsp.MarkupContent(kind=lsp.MarkupKind.Markdown, value=markdown), range=range_)
|
||||||
|
|
||||||
|
|
||||||
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DEFINITION)
|
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DEFINITION)
|
||||||
@@ -798,7 +847,8 @@ def goto_definition(params: lsp.DefinitionParams):
|
|||||||
return [target]
|
return [target]
|
||||||
context = _navigation_context(params.text_document.uri, params.position)
|
context = _navigation_context(params.text_document.uri, params.position)
|
||||||
if context is None:
|
if context is None:
|
||||||
return None
|
symbol = _tcl_def_symbol(params.text_document.uri, params.position, _word_at(params.text_document.uri, params.position))
|
||||||
|
return symbol and _sorted_locations(def_definition_locations(symbol[0], symbol[1])) or None
|
||||||
|
|
||||||
indexes, definitions, _, identity = context
|
indexes, definitions, _, identity = context
|
||||||
target = _def_target(identity)
|
target = _def_target(identity)
|
||||||
@@ -842,7 +892,11 @@ def _sorted_locations(locations: list[lsp.Location]) -> list[lsp.Location]:
|
|||||||
def references(params: lsp.ReferenceParams) -> list[lsp.Location]:
|
def references(params: lsp.ReferenceParams) -> list[lsp.Location]:
|
||||||
context = _navigation_context(params.text_document.uri, params.position)
|
context = _navigation_context(params.text_document.uri, params.position)
|
||||||
if context is None:
|
if context is None:
|
||||||
return []
|
symbol = _tcl_def_symbol(params.text_document.uri, params.position, _word_at(params.text_document.uri, params.position))
|
||||||
|
if symbol is None:
|
||||||
|
return []
|
||||||
|
indexes, _ = LSP_SERVER.navigation_state()
|
||||||
|
return all_def_target_locations(symbol[0], indexes, symbol[1], params.context.include_declaration)
|
||||||
|
|
||||||
indexes, definitions, _, identity = context
|
indexes, definitions, _, identity = context
|
||||||
target = _def_target(identity)
|
target = _def_target(identity)
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ from tools import checks, incremental_parse, parser
|
|||||||
from tools.completion_items import CompletionCollector
|
from tools.completion_items import CompletionCollector
|
||||||
from tools.tcloo_symbols import class_completion_items
|
from tools.tcloo_symbols import class_completion_items
|
||||||
from tools.tcloo_completion import indexed_classes
|
from tools.tcloo_completion import indexed_classes
|
||||||
|
from tools.def_flow import WrapperTable, build_wrapper_table
|
||||||
from tools.def_symbols import ADDRESS, BLOCK_TEMPLATE, DefDocument, parse_def_document, read_def_source
|
from tools.def_symbols import ADDRESS, BLOCK_TEMPLATE, DefDocument, parse_def_document, read_def_source
|
||||||
from tools.file_sourcing import get_all_psc_files, psc_defined_event_files, psc_script_files
|
from tools.file_sourcing import get_all_psc_files, psc_defined_event_files, psc_script_files
|
||||||
from tools.formatter import NxFormatter as Formatter
|
from tools.formatter import NxFormatter as Formatter
|
||||||
@@ -97,6 +98,7 @@ class TclLanguageServer(LanguageServer):
|
|||||||
self._definition_identities_cache: tuple[
|
self._definition_identities_cache: tuple[
|
||||||
int, frozenset[SymbolIdentity]
|
int, frozenset[SymbolIdentity]
|
||||||
] = (-1, frozenset())
|
] = (-1, frozenset())
|
||||||
|
self._def_wrapper_cache: tuple[int, WrapperTable] = (-1, {})
|
||||||
self._proc_metadata_cache: dict[
|
self._proc_metadata_cache: dict[
|
||||||
str, tuple[int, dict[str, list[str]], dict[str, str]]
|
str, tuple[int, dict[str, list[str]], dict[str, str]]
|
||||||
] = {}
|
] = {}
|
||||||
@@ -213,6 +215,7 @@ class TclLanguageServer(LanguageServer):
|
|||||||
self._workspace_completion_cache = (-1, ())
|
self._workspace_completion_cache = (-1, ())
|
||||||
self._custom_function_names_cache = (-1, frozenset())
|
self._custom_function_names_cache = (-1, frozenset())
|
||||||
self._definition_identities_cache = (-1, frozenset())
|
self._definition_identities_cache = (-1, frozenset())
|
||||||
|
self._def_wrapper_cache = (-1, {})
|
||||||
self._proc_metadata_cache.clear()
|
self._proc_metadata_cache.clear()
|
||||||
self._custom_inlay_cache.clear()
|
self._custom_inlay_cache.clear()
|
||||||
|
|
||||||
@@ -290,6 +293,7 @@ class TclLanguageServer(LanguageServer):
|
|||||||
label=name,
|
label=name,
|
||||||
kind=item_kind,
|
kind=item_kind,
|
||||||
detail=f"{description} ({pathlib.Path(path).name})",
|
detail=f"{description} ({pathlib.Path(path).name})",
|
||||||
|
data={"def": [kind, name]},
|
||||||
)
|
)
|
||||||
for path, document in self.def_documents_snapshot().items()
|
for path, document in self.def_documents_snapshot().items()
|
||||||
for name in document.names(kind)
|
for name in document.names(kind)
|
||||||
@@ -629,6 +633,17 @@ class TclLanguageServer(LanguageServer):
|
|||||||
)
|
)
|
||||||
return dict(self.navigation_indexes), definitions
|
return dict(self.navigation_indexes), definitions
|
||||||
|
|
||||||
|
def def_wrapper_table(self) -> WrapperTable:
|
||||||
|
"""Proc arguments that take .def names, cached by index generation."""
|
||||||
|
with self._index_lock:
|
||||||
|
generation, table = self._def_wrapper_cache
|
||||||
|
if generation != self._index_generation:
|
||||||
|
table = build_wrapper_table(
|
||||||
|
flow for index in self.navigation_indexes.values() for flow in index.def_flows
|
||||||
|
)
|
||||||
|
self._def_wrapper_cache = (self._index_generation, table)
|
||||||
|
return table
|
||||||
|
|
||||||
def _begin_index_update(self, filepath: str, version: int | None) -> int | None:
|
def _begin_index_update(self, filepath: str, version: int | None) -> int | None:
|
||||||
with self._index_lock:
|
with self._index_lock:
|
||||||
indexed_version = self._index_versions.get(filepath)
|
indexed_version = self._index_versions.get(filepath)
|
||||||
|
|||||||
@@ -0,0 +1,340 @@
|
|||||||
|
"""Follow .def block template and address names through variables and procs.
|
||||||
|
|
||||||
|
A name is only a .def symbol where it provably reaches an NX command taking one:
|
||||||
|
directly as an argument (``MOM_do_template steady_rest``), through a variable
|
||||||
|
of the same scope (``set t steady_rest; MOM_do_template $t``) or through the
|
||||||
|
parameter of a proc that passes it on (``LIB_SPF_call_cycle absolute_mode``).
|
||||||
|
Derived names are resolved for hover and definition only, never renamed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from collections import defaultdict
|
||||||
|
from collections.abc import Iterable, Iterator
|
||||||
|
|
||||||
|
import lsprotocol.types as lsp
|
||||||
|
from tclint.syntax_tree import BracedWord, Command, CommandSub, List, Node, QuotedWord, Script, VarSub
|
||||||
|
from tools.tcl_command_completion import TCL_COMMAND_NAMES
|
||||||
|
|
||||||
|
ROOT_NAMESPACE = "::"
|
||||||
|
DEF_BLOCK_TEMPLATE = "block_template"
|
||||||
|
DEF_ADDRESS = "address"
|
||||||
|
DEF_SYMBOL_KINDS = frozenset({DEF_BLOCK_TEMPLATE, DEF_ADDRESS})
|
||||||
|
|
||||||
|
# NX commands taking .def names: command -> (first argument index, last index or
|
||||||
|
# None for all following arguments, kind).
|
||||||
|
_DEF_ARGUMENTS: dict[str, tuple[tuple[int, int | None, str], ...]] = {
|
||||||
|
"MOM_do_template": ((0, 0, DEF_BLOCK_TEMPLATE),),
|
||||||
|
"MOM_add_to_block_buffer": ((0, 0, DEF_BLOCK_TEMPLATE),),
|
||||||
|
"MOM_polar_motion": ((0, 0, DEF_BLOCK_TEMPLATE),),
|
||||||
|
"MOM_force_block": ((1, None, DEF_BLOCK_TEMPLATE),),
|
||||||
|
"MOM_ask_address_value": ((0, 0, DEF_ADDRESS),),
|
||||||
|
"MOM_add_to_address_buffer": ((0, 0, DEF_ADDRESS),),
|
||||||
|
"MOM_enable_address": ((0, None, DEF_ADDRESS),),
|
||||||
|
"MOM_disable_address": ((0, None, DEF_ADDRESS),),
|
||||||
|
"MOM_force": ((1, None, DEF_ADDRESS),),
|
||||||
|
"MOM_suppress": ((1, None, DEF_ADDRESS),),
|
||||||
|
"MOM_incremental": ((1, None, DEF_ADDRESS),),
|
||||||
|
}
|
||||||
|
_DEFINITION_ELEMENT_COMMANDS = frozenset({"MOM_ask_definition_element", "MOM_has_definition_element"})
|
||||||
|
_DEFINITION_ELEMENT_KINDS = {"ADDRESS": DEF_ADDRESS, "BLOCK": DEF_BLOCK_TEMPLATE}
|
||||||
|
|
||||||
|
# Commands that never pass a .def name on to a proc parameter.
|
||||||
|
_NON_FORWARDING = (
|
||||||
|
frozenset(TCL_COMMAND_NAMES)
|
||||||
|
| frozenset(_DEF_ARGUMENTS)
|
||||||
|
| _DEFINITION_ELEMENT_COMMANDS
|
||||||
|
| frozenset({
|
||||||
|
"set", "unset", "puts", "expr", "return", "incr", "append", "lappend", "list", "lindex", "lrange",
|
||||||
|
"llength", "lsearch", "lsort", "lreverse", "lassign", "concat", "join", "split", "format", "regsub",
|
||||||
|
"regexp", "string", "if", "while", "for", "foreach", "lmap", "switch", "catch", "eval", "uplevel",
|
||||||
|
"upvar", "global", "variable", "info", "array", "dict", "subst", "error", "proc", "namespace",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
# Commands returning (elements of) their first argument's list value.
|
||||||
|
_LIST_ACCESSORS = frozenset({"lindex", "lrange", "lsort", "lreverse", "lsearch"})
|
||||||
|
_LIST_WORD_RE = re.compile(r'"([^"\s{}]*)"|([^\s"{}]+)')
|
||||||
|
|
||||||
|
# ("def", kind) or ("call", routine, argument index) where a variable ends up.
|
||||||
|
FlowTarget = tuple
|
||||||
|
# Fact: ("sink", variable, target) or ("edge", destination, source variable).
|
||||||
|
FlowFact = tuple
|
||||||
|
# Per proc: ((parameter index, ("def", kind) | ("call", qualified, fallback, index)), ...)
|
||||||
|
ProcDefFlows = tuple[tuple[int, tuple], ...]
|
||||||
|
WrapperTable = dict[str, dict[int, frozenset[str]]]
|
||||||
|
|
||||||
|
|
||||||
|
def static_contents(node: Node | None) -> str | None:
|
||||||
|
value = getattr(node, "contents", None)
|
||||||
|
return value if isinstance(value, str) else None
|
||||||
|
|
||||||
|
|
||||||
|
def def_argument_kinds(command: Command) -> list[tuple[Node, str]]:
|
||||||
|
"""Return the arguments of ``command`` that name a .def block template or address."""
|
||||||
|
routine = static_contents(command.routine)
|
||||||
|
if routine in _DEFINITION_ELEMENT_COMMANDS:
|
||||||
|
kind = _DEFINITION_ELEMENT_KINDS.get((static_contents(command.args[0]) or "").upper()) if command.args else None
|
||||||
|
return [(command.args[1], kind)] if kind and len(command.args) >= 2 else []
|
||||||
|
result = []
|
||||||
|
for first, last, kind in _DEF_ARGUMENTS.get(routine or "", ()):
|
||||||
|
for position, argument in enumerate(command.args):
|
||||||
|
if position >= first and (last is None or position <= last):
|
||||||
|
result.append((argument, kind))
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def def_name(node: Node) -> str | None:
|
||||||
|
name = static_contents(node)
|
||||||
|
if not name or node.contents_pos is None or any(char.isspace() or char in "$[]{}\\\"" for char in name):
|
||||||
|
return None
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
def qualify(name: str, namespace: str) -> str:
|
||||||
|
if name.startswith("::"):
|
||||||
|
return name
|
||||||
|
return f"::{name}" if namespace == ROOT_NAMESPACE else f"{namespace}::{name}"
|
||||||
|
|
||||||
|
|
||||||
|
def _variable_reference(node: Node) -> str | None:
|
||||||
|
"""Name of the scalar variable ``node`` consists of: ``$v`` or ``"$v"``."""
|
||||||
|
if isinstance(node, QuotedWord) and len(node.children) == 1:
|
||||||
|
node = node.children[0]
|
||||||
|
if isinstance(node, VarSub) and isinstance(node.value, str) and "(" not in node.value:
|
||||||
|
return node.value
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _value_sources(node: Node) -> list[str]:
|
||||||
|
"""Variables whose value or list elements ``node`` copies."""
|
||||||
|
variable = _variable_reference(node)
|
||||||
|
if variable is not None:
|
||||||
|
return [variable]
|
||||||
|
if isinstance(node, CommandSub) and len(node.children) == 1 and isinstance(node.children[0], Command):
|
||||||
|
inner = node.children[0]
|
||||||
|
routine = static_contents(inner.routine)
|
||||||
|
if routine in _LIST_ACCESSORS and inner.args:
|
||||||
|
return _value_sources(inner.args[0])
|
||||||
|
if routine in {"list", "concat"}:
|
||||||
|
return [source for argument in inner.args for source in _value_sources(argument)]
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def _bound_names(node: Node) -> list[str]:
|
||||||
|
nodes = node.children if isinstance(node, List) else [node]
|
||||||
|
names = [static_contents(child) for child in nodes]
|
||||||
|
if len(names) == 1 and names[0] and " " in names[0]:
|
||||||
|
return names[0].split()
|
||||||
|
return [name for name in names if name]
|
||||||
|
|
||||||
|
|
||||||
|
def command_flow_facts(command: Command) -> list[FlowFact]:
|
||||||
|
"""Facts on how ``command`` moves variable values towards .def arguments."""
|
||||||
|
routine = static_contents(command.routine)
|
||||||
|
args = command.args
|
||||||
|
facts: list[FlowFact] = [
|
||||||
|
("sink", variable, ("def", kind))
|
||||||
|
for node, kind in def_argument_kinds(command)
|
||||||
|
if (variable := _variable_reference(node)) is not None
|
||||||
|
]
|
||||||
|
if routine == "set" and len(args) == 2:
|
||||||
|
destination = static_contents(args[0])
|
||||||
|
if destination:
|
||||||
|
facts.extend(("edge", destination, source) for source in _value_sources(args[1]))
|
||||||
|
elif routine == "lappend" and args:
|
||||||
|
destination = static_contents(args[0])
|
||||||
|
if destination:
|
||||||
|
facts.extend(("edge", destination, source) for argument in args[1:] for source in _value_sources(argument))
|
||||||
|
elif routine in {"foreach", "lmap"} and len(args) >= 3:
|
||||||
|
for position in range(0, len(args) - 1, 2):
|
||||||
|
sources = _value_sources(args[position + 1])
|
||||||
|
facts.extend(("edge", name, source) for name in _bound_names(args[position]) for source in sources)
|
||||||
|
elif routine == "lassign" and args:
|
||||||
|
sources = _value_sources(args[0])
|
||||||
|
facts.extend(("edge", name, source) for node in args[1:] if (name := static_contents(node)) for source in sources)
|
||||||
|
elif routine and routine not in _NON_FORWARDING:
|
||||||
|
facts.extend(
|
||||||
|
("sink", variable, ("call", routine, position))
|
||||||
|
for position, argument in enumerate(args)
|
||||||
|
if (variable := _variable_reference(argument)) is not None
|
||||||
|
)
|
||||||
|
return facts
|
||||||
|
|
||||||
|
|
||||||
|
def solve_flow(facts: Iterable[FlowFact]) -> dict[str, set[FlowTarget]]:
|
||||||
|
"""Map each variable to the .def arguments and proc parameters it reaches."""
|
||||||
|
targets: dict[str, set[FlowTarget]] = defaultdict(set)
|
||||||
|
sources: dict[str, set[str]] = defaultdict(set)
|
||||||
|
for fact in facts:
|
||||||
|
if fact[0] == "sink":
|
||||||
|
targets[fact[1]].add(fact[2])
|
||||||
|
elif fact[1] != fact[2]:
|
||||||
|
sources[fact[1]].add(fact[2])
|
||||||
|
pending = [variable for variable in targets if variable in sources]
|
||||||
|
while pending:
|
||||||
|
destination = pending.pop()
|
||||||
|
for source in sources.get(destination, ()):
|
||||||
|
before = len(targets[source])
|
||||||
|
targets[source] |= targets[destination]
|
||||||
|
if len(targets[source]) != before:
|
||||||
|
pending.append(source)
|
||||||
|
return targets
|
||||||
|
|
||||||
|
|
||||||
|
def proc_def_flows(facts: Iterable[FlowFact], parameters: list[str], namespace: str) -> ProcDefFlows:
|
||||||
|
"""Where the parameters of a proc end up, with qualified callee names."""
|
||||||
|
targets = solve_flow(facts)
|
||||||
|
flows = []
|
||||||
|
for position, parameter in enumerate(parameters):
|
||||||
|
if parameter == "args" and position == len(parameters) - 1:
|
||||||
|
break
|
||||||
|
for target in targets.get(parameter, ()):
|
||||||
|
if target[0] == "call":
|
||||||
|
target = ("call", qualify(target[1], namespace), qualify(target[1], ROOT_NAMESPACE), target[2])
|
||||||
|
flows.append((position, target))
|
||||||
|
return tuple(sorted(flows))
|
||||||
|
|
||||||
|
|
||||||
|
def build_wrapper_table(procs: Iterable[tuple[str, ProcDefFlows]]) -> WrapperTable:
|
||||||
|
"""Resolve which proc arguments take .def names, following nested wrappers."""
|
||||||
|
kinds: dict[str, dict[int, set[str]]] = defaultdict(lambda: defaultdict(set))
|
||||||
|
calls = []
|
||||||
|
for proc, flows in procs:
|
||||||
|
for position, target in flows:
|
||||||
|
if target[0] == "def":
|
||||||
|
kinds[proc][position].add(target[1])
|
||||||
|
else:
|
||||||
|
calls.append((proc, position, target[1], target[2], target[3]))
|
||||||
|
changed = True
|
||||||
|
while changed:
|
||||||
|
changed = False
|
||||||
|
for proc, position, callee, fallback, callee_position in calls:
|
||||||
|
entry = kinds.get(callee) or kinds.get(fallback)
|
||||||
|
found = entry.get(callee_position) if entry else None
|
||||||
|
if found and not found <= kinds[proc][position]:
|
||||||
|
kinds[proc][position] |= found
|
||||||
|
changed = True
|
||||||
|
return {
|
||||||
|
proc: {position: frozenset(names) for position, names in positions.items() if names}
|
||||||
|
for proc, positions in kinds.items()
|
||||||
|
if any(positions.values())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _wrapper_kinds(table: WrapperTable, routine: str, position: int, namespace: str) -> frozenset[str]:
|
||||||
|
entry = table.get(qualify(routine, namespace)) or table.get(qualify(routine, ROOT_NAMESPACE))
|
||||||
|
return entry.get(position, frozenset()) if entry else frozenset()
|
||||||
|
|
||||||
|
|
||||||
|
def _target_kinds(target: FlowTarget, table: WrapperTable, namespace: str) -> frozenset[str]:
|
||||||
|
if target[0] == "def":
|
||||||
|
return frozenset({target[1]})
|
||||||
|
return _wrapper_kinds(table, target[1], target[2], namespace)
|
||||||
|
|
||||||
|
|
||||||
|
def _scope_commands(script: Node) -> Iterator[Command]:
|
||||||
|
"""Commands of one scope, without the bodies of procs defined in it."""
|
||||||
|
for child in getattr(script, "children", []):
|
||||||
|
if isinstance(child, Command):
|
||||||
|
yield child
|
||||||
|
if static_contents(child.routine) == "proc":
|
||||||
|
continue
|
||||||
|
yield from _scope_commands(child)
|
||||||
|
|
||||||
|
|
||||||
|
def _contains(node: Node, point: tuple[int, int]) -> bool:
|
||||||
|
return node.pos is not None and node.end_pos is not None and node.pos <= point < node.end_pos
|
||||||
|
|
||||||
|
|
||||||
|
def _path_at(tree: Node, point: tuple[int, int]) -> list[Node]:
|
||||||
|
path = [tree]
|
||||||
|
while True:
|
||||||
|
child = next((child for child in getattr(path[-1], "children", []) if _contains(child, point)), None)
|
||||||
|
if child is None:
|
||||||
|
return path
|
||||||
|
path.append(child)
|
||||||
|
|
||||||
|
|
||||||
|
def _literal_at(node: Node, point: tuple[int, int]) -> tuple[str, lsp.Range] | None:
|
||||||
|
"""The single name ``node`` holds, or the list element of a braced word at ``point``."""
|
||||||
|
if isinstance(node, BracedWord):
|
||||||
|
contents = static_contents(node)
|
||||||
|
if contents is None or node.contents_pos is None:
|
||||||
|
return None
|
||||||
|
line, column = node.contents_pos
|
||||||
|
for match in _LIST_WORD_RE.finditer(contents):
|
||||||
|
start = match.start(1) if match.group(1) is not None else match.start(2)
|
||||||
|
name = match.group(1) if match.group(1) is not None else match.group(2)
|
||||||
|
before = contents[:start]
|
||||||
|
element_line = line + before.count("\n")
|
||||||
|
element_column = (start - before.rfind("\n") if "\n" in before else column + start)
|
||||||
|
if element_line == point[0] and element_column <= point[1] < element_column + len(name):
|
||||||
|
return name, _range(element_line, element_column, name)
|
||||||
|
return None
|
||||||
|
name = def_name(node)
|
||||||
|
if name is None:
|
||||||
|
return None
|
||||||
|
line, column = node.contents_pos
|
||||||
|
return name, _range(line, column, name)
|
||||||
|
|
||||||
|
|
||||||
|
def _range(line: int, column: int, name: str) -> lsp.Range:
|
||||||
|
return lsp.Range(
|
||||||
|
start=lsp.Position(line=line - 1, character=column - 1),
|
||||||
|
end=lsp.Position(line=line - 1, character=column - 1 + len(name)),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _literal_targets(commands: list[Command], values: list[Node]) -> tuple[list[str], list[FlowTarget]]:
|
||||||
|
"""Variables and proc arguments a literal flows into; ``values`` are its enclosing words."""
|
||||||
|
command, value = commands[-1], values[-1]
|
||||||
|
routine = static_contents(command.routine)
|
||||||
|
args = list(command.args)
|
||||||
|
position = next((index for index, argument in enumerate(args) if argument is value), None)
|
||||||
|
if position is None:
|
||||||
|
return [], []
|
||||||
|
if routine in {"list", "concat"} and len(commands) >= 2 and isinstance(values[-2], CommandSub):
|
||||||
|
return _literal_targets(commands[:-1], values[:-1])
|
||||||
|
if routine == "set" and position == 1:
|
||||||
|
destination = static_contents(args[0])
|
||||||
|
return ([destination] if destination else []), []
|
||||||
|
if routine == "lappend" and position >= 1:
|
||||||
|
destination = static_contents(args[0])
|
||||||
|
return ([destination] if destination else []), []
|
||||||
|
if routine in {"foreach", "lmap"} and position % 2 == 1 and position < len(args) - 1:
|
||||||
|
return _bound_names(args[position - 1]), []
|
||||||
|
if routine and routine not in _NON_FORWARDING:
|
||||||
|
return [], [("call", routine, position)]
|
||||||
|
return [], []
|
||||||
|
|
||||||
|
|
||||||
|
def derived_def_symbol(
|
||||||
|
tree: Node, position: lsp.Position, table: WrapperTable
|
||||||
|
) -> tuple[frozenset[str], str, lsp.Range] | None:
|
||||||
|
"""Kinds, name and range of a literal that reaches a .def argument indirectly."""
|
||||||
|
point = (position.line + 1, position.character + 1)
|
||||||
|
path = _path_at(tree, point)
|
||||||
|
commands: list[Command] = []
|
||||||
|
values: list[Node] = []
|
||||||
|
scope: Node = tree
|
||||||
|
namespace = ROOT_NAMESPACE
|
||||||
|
for parent, child in zip(path, path[1:]):
|
||||||
|
if isinstance(parent, Command):
|
||||||
|
commands.append(parent)
|
||||||
|
values.append(child)
|
||||||
|
if static_contents(parent.routine) == "proc" and len(parent.args) >= 3 and child is parent.args[2]:
|
||||||
|
scope = child
|
||||||
|
name = qualify(static_contents(parent.args[0]) or "", ROOT_NAMESPACE)
|
||||||
|
namespace = name.rsplit("::", 1)[0] or ROOT_NAMESPACE
|
||||||
|
if not commands or isinstance(values[-1], Script):
|
||||||
|
return None
|
||||||
|
literal = _literal_at(values[-1], point)
|
||||||
|
if literal is None:
|
||||||
|
return None
|
||||||
|
variables, targets = _literal_targets(commands, values)
|
||||||
|
if variables:
|
||||||
|
scope_targets = solve_flow(fact for command in _scope_commands(scope) for fact in command_flow_facts(command))
|
||||||
|
targets.extend(target for variable in variables for target in scope_targets.get(variable, ()))
|
||||||
|
kinds = frozenset(kind for target in targets for kind in _target_kinds(target, table, namespace))
|
||||||
|
return (kinds, *literal) if kinds else None
|
||||||
@@ -5,6 +5,16 @@ from pathlib import Path
|
|||||||
|
|
||||||
import lsprotocol.types as lsp
|
import lsprotocol.types as lsp
|
||||||
from tclint.syntax_tree import Command, List, Node, QuotedWord, Script, VarSub
|
from tclint.syntax_tree import Command, List, Node, QuotedWord, Script, VarSub
|
||||||
|
from tools.def_flow import ( # noqa: F401 (re-exported)
|
||||||
|
DEF_ADDRESS,
|
||||||
|
DEF_BLOCK_TEMPLATE,
|
||||||
|
DEF_SYMBOL_KINDS,
|
||||||
|
ProcDefFlows,
|
||||||
|
command_flow_facts,
|
||||||
|
def_argument_kinds,
|
||||||
|
proc_def_flows,
|
||||||
|
)
|
||||||
|
from tools.def_flow import def_name as _def_name
|
||||||
from tools.variable_names import array_key_parts, variable_name
|
from tools.variable_names import array_key_parts, variable_name
|
||||||
|
|
||||||
ROOT_NAMESPACE = "::"
|
ROOT_NAMESPACE = "::"
|
||||||
@@ -39,6 +49,8 @@ class FileSymbolIndex:
|
|||||||
uri: str
|
uri: str
|
||||||
occurrences: tuple[SymbolOccurrence, ...]
|
occurrences: tuple[SymbolOccurrence, ...]
|
||||||
document_range: lsp.Range | None = None
|
document_range: lsp.Range | None = None
|
||||||
|
# Procs whose parameters reach .def arguments: (qualified proc name, flows).
|
||||||
|
def_flows: tuple[tuple[str, ProcDefFlows], ...] = ()
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -114,49 +126,6 @@ def _name_range(node: Node, raw_name: str, *, variable_sub: bool = False) -> lsp
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
DEF_BLOCK_TEMPLATE = "block_template"
|
|
||||||
DEF_ADDRESS = "address"
|
|
||||||
DEF_SYMBOL_KINDS = frozenset({DEF_BLOCK_TEMPLATE, DEF_ADDRESS})
|
|
||||||
|
|
||||||
# NX commands taking .def names: command -> (first argument index, last index or
|
|
||||||
# None for all following arguments, kind).
|
|
||||||
_DEF_ARGUMENTS: dict[str, tuple[tuple[int, int | None, str], ...]] = {
|
|
||||||
"MOM_do_template": ((0, 0, DEF_BLOCK_TEMPLATE),),
|
|
||||||
"MOM_add_to_block_buffer": ((0, 0, DEF_BLOCK_TEMPLATE),),
|
|
||||||
"MOM_polar_motion": ((0, 0, DEF_BLOCK_TEMPLATE),),
|
|
||||||
"MOM_force_block": ((1, None, DEF_BLOCK_TEMPLATE),),
|
|
||||||
"MOM_ask_address_value": ((0, 0, DEF_ADDRESS),),
|
|
||||||
"MOM_add_to_address_buffer": ((0, 0, DEF_ADDRESS),),
|
|
||||||
"MOM_enable_address": ((0, None, DEF_ADDRESS),),
|
|
||||||
"MOM_disable_address": ((0, None, DEF_ADDRESS),),
|
|
||||||
"MOM_force": ((1, None, DEF_ADDRESS),),
|
|
||||||
"MOM_suppress": ((1, None, DEF_ADDRESS),),
|
|
||||||
"MOM_incremental": ((1, None, DEF_ADDRESS),),
|
|
||||||
}
|
|
||||||
_DEFINITION_ELEMENT_KINDS = {"ADDRESS": DEF_ADDRESS, "BLOCK": DEF_BLOCK_TEMPLATE}
|
|
||||||
|
|
||||||
|
|
||||||
def def_argument_kinds(command: Command) -> list[tuple[Node, str]]:
|
|
||||||
"""Return the arguments of ``command`` that name a .def block template or address."""
|
|
||||||
routine = _static_contents(command.routine)
|
|
||||||
if routine in {"MOM_ask_definition_element", "MOM_has_definition_element"}:
|
|
||||||
kind = _DEFINITION_ELEMENT_KINDS.get((_static_contents(command.args[0]) or "").upper()) if command.args else None
|
|
||||||
return [(command.args[1], kind)] if kind and len(command.args) >= 2 else []
|
|
||||||
result = []
|
|
||||||
for first, last, kind in _DEF_ARGUMENTS.get(routine or "", ()):
|
|
||||||
for position, argument in enumerate(command.args):
|
|
||||||
if position >= first and (last is None or position <= last):
|
|
||||||
result.append((argument, kind))
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def _def_name(node: Node) -> str | None:
|
|
||||||
name = _static_contents(node)
|
|
||||||
if not name or node.contents_pos is None or any(char.isspace() or char in "$[]{}\\\"" for char in name):
|
|
||||||
return None
|
|
||||||
return name
|
|
||||||
|
|
||||||
|
|
||||||
def _node_range(node: Node) -> lsp.Range | None:
|
def _node_range(node: Node) -> lsp.Range | None:
|
||||||
if node.pos is None or node.end_pos is None:
|
if node.pos is None or node.end_pos is None:
|
||||||
return None
|
return None
|
||||||
@@ -311,6 +280,9 @@ def build_file_symbol_index(
|
|||||||
# Most occurrences repeat a few identities; sharing one object per identity
|
# Most occurrences repeat a few identities; sharing one object per identity
|
||||||
# keeps the index (and its persistent cache) small.
|
# keeps the index (and its persistent cache) small.
|
||||||
identities: dict[SymbolIdentity, SymbolIdentity] = {}
|
identities: dict[SymbolIdentity, SymbolIdentity] = {}
|
||||||
|
def_flows: list[tuple[str, ProcDefFlows]] = []
|
||||||
|
# Flow facts of the procs being walked, innermost last.
|
||||||
|
flow_facts: list[list] = []
|
||||||
|
|
||||||
def shared(identity: SymbolIdentity | None) -> SymbolIdentity | None:
|
def shared(identity: SymbolIdentity | None) -> SymbolIdentity | None:
|
||||||
return None if identity is None else identities.setdefault(identity, identity)
|
return None if identity is None else identities.setdefault(identity, identity)
|
||||||
@@ -427,11 +399,13 @@ def build_file_symbol_index(
|
|||||||
)
|
)
|
||||||
|
|
||||||
parameters = command.args[1]
|
parameters = command.args[1]
|
||||||
|
parameter_names = []
|
||||||
for parameter in getattr(parameters, "children", []):
|
for parameter in getattr(parameters, "children", []):
|
||||||
parameter_node = parameter
|
parameter_node = parameter
|
||||||
if isinstance(parameter, List) and parameter.children:
|
if isinstance(parameter, List) and parameter.children:
|
||||||
parameter_node = parameter.children[0]
|
parameter_node = parameter.children[0]
|
||||||
parameter_name = _static_contents(parameter_node)
|
parameter_name = _static_contents(parameter_node)
|
||||||
|
parameter_names.append(parameter_name or "")
|
||||||
if parameter_name:
|
if parameter_name:
|
||||||
add_variable(
|
add_variable(
|
||||||
parameter_node,
|
parameter_node,
|
||||||
@@ -440,7 +414,12 @@ def build_file_symbol_index(
|
|||||||
is_definition=True,
|
is_definition=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
flow_facts.append([])
|
||||||
walk_script(body, proc_scope)
|
walk_script(body, proc_scope)
|
||||||
|
facts = flow_facts.pop()
|
||||||
|
flows = proc_def_flows(facts, parameter_names, proc_namespace) if facts else ()
|
||||||
|
if flows:
|
||||||
|
def_flows.append((proc_identity.name, flows))
|
||||||
|
|
||||||
def walk_namespace(command: Command, scope: _Scope) -> bool:
|
def walk_namespace(command: Command, scope: _Scope) -> bool:
|
||||||
if len(command.args) < 3 or _static_contents(command.args[0]) != "eval":
|
if len(command.args) < 3 or _static_contents(command.args[0]) != "eval":
|
||||||
@@ -478,6 +457,8 @@ def build_file_symbol_index(
|
|||||||
|
|
||||||
if routine:
|
if routine:
|
||||||
add_proc(command.routine, routine, scope, is_definition=False)
|
add_proc(command.routine, routine, scope, is_definition=False)
|
||||||
|
if flow_facts:
|
||||||
|
flow_facts[-1].extend(command_flow_facts(command))
|
||||||
|
|
||||||
for node, kind in def_argument_kinds(command):
|
for node, kind in def_argument_kinds(command):
|
||||||
name = _def_name(node)
|
name = _def_name(node)
|
||||||
@@ -550,6 +531,7 @@ def build_file_symbol_index(
|
|||||||
uri=uri,
|
uri=uri,
|
||||||
occurrences=tuple(occurrences),
|
occurrences=tuple(occurrences),
|
||||||
document_range=_node_range(tree),
|
document_range=_node_range(tree),
|
||||||
|
def_flows=tuple(def_flows),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -47,20 +47,9 @@ def _document(path: Path, source: str) -> TextDocument:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _completion_server(
|
def _completion_server(tmp_path: Path, monkeypatch) -> tuple[TclLanguageServer, TextDocument, str]:
|
||||||
tmp_path: Path, monkeypatch
|
|
||||||
) -> tuple[TclLanguageServer, TextDocument, str]:
|
|
||||||
declared_builtin = standard_items.nx_variables[0].label
|
declared_builtin = standard_items.nx_variables[0].label
|
||||||
current_source = (
|
current_source = f"set globalValue 1\nproc localProc {{}} {{ return }}\nproc caller {{argument}} {{\n global {declared_builtin}\n set localValue 2\n puts $local\n localP\n}}\n"
|
||||||
"set globalValue 1\n"
|
|
||||||
"proc localProc {} { return }\n"
|
|
||||||
"proc caller {argument} {\n"
|
|
||||||
f" global {declared_builtin}\n"
|
|
||||||
" set localValue 2\n"
|
|
||||||
" puts $local\n"
|
|
||||||
" localP\n"
|
|
||||||
"}\n"
|
|
||||||
)
|
|
||||||
workspace_source = """set ::workspaceValue 1
|
workspace_source = """set ::workspaceValue 1
|
||||||
proc workspaceProc {} { return }
|
proc workspaceProc {} { return }
|
||||||
"""
|
"""
|
||||||
@@ -113,14 +102,10 @@ def test_unset_space_shows_options_then_variables(tmp_path, monkeypatch):
|
|||||||
items = _complete(document, lsp.Position(line=1, character=len(tail)))
|
items = _complete(document, lsp.Position(line=1, character=len(tail)))
|
||||||
labels = {item.label for item in items}
|
labels = {item.label for item in items}
|
||||||
if tail in {"unset ", "unset -"}:
|
if tail in {"unset ", "unset -"}:
|
||||||
assert labels == {"-nocomplain", "--"}
|
assert labels == {"nocomplain"}
|
||||||
else:
|
else:
|
||||||
assert "globalValue" in labels
|
assert "globalValue" in labels
|
||||||
assert "-nocomplain" not in labels
|
assert "-nocomplain" not in labels
|
||||||
if tail == "unset -nocomplain ":
|
|
||||||
assert "--" in labels
|
|
||||||
else:
|
|
||||||
assert "--" not in labels
|
|
||||||
|
|
||||||
|
|
||||||
def test_array_keys_complete_in_set_and_substitution(tmp_path: Path, monkeypatch):
|
def test_array_keys_complete_in_set_and_substitution(tmp_path: Path, monkeypatch):
|
||||||
@@ -130,19 +115,16 @@ def test_array_keys_complete_in_set_and_substitution(tmp_path: Path, monkeypatch
|
|||||||
"set ::lib_flag(enabled) 1\nset ::lib_flag(external) 1\n",
|
"set ::lib_flag(enabled) 1\nset ::lib_flag(external) 1\n",
|
||||||
)
|
)
|
||||||
assert server.update_poco_completion_for_file(workspace)
|
assert server.update_poco_completion_for_file(workspace)
|
||||||
source = (
|
source = "set lib_flag(enabled) 0\nset lib_flag(empty) 1\nset other(wrong) 1\nproc hidden {} { set lib_flag(private) 1 }\nset lib_flag()\nputs $lib_flag(en)\nputs 😀; set lib_flag(em\n"
|
||||||
"set lib_flag(enabled) 0\n"
|
|
||||||
"set lib_flag(empty) 1\n"
|
|
||||||
"set other(wrong) 1\n"
|
|
||||||
"proc hidden {} { set lib_flag(private) 1 }\n"
|
|
||||||
"set lib_flag()\n"
|
|
||||||
"puts $lib_flag(en)\n"
|
|
||||||
"puts 😀; set lib_flag(em\n"
|
|
||||||
)
|
|
||||||
current = _document(tmp_path / "arrays-current.tcl", source)
|
current = _document(tmp_path / "arrays-current.tcl", source)
|
||||||
server.workspace.put_text_document(lsp.TextDocumentItem(
|
server.workspace.put_text_document(
|
||||||
uri=current.uri, language_id="tcl", version=1, text=source,
|
lsp.TextDocumentItem(
|
||||||
))
|
uri=current.uri,
|
||||||
|
language_id="tcl",
|
||||||
|
version=1,
|
||||||
|
text=source,
|
||||||
|
)
|
||||||
|
)
|
||||||
assert server.update_poco_completion_for_file(current)
|
assert server.update_poco_completion_for_file(current)
|
||||||
items = _complete(current, _position_after(source, "set lib_flag(", 3))
|
items = _complete(current, _position_after(source, "set lib_flag(", 3))
|
||||||
assert [item.label for item in items] == ["empty", "enabled", "external"]
|
assert [item.label for item in items] == ["empty", "enabled", "external"]
|
||||||
@@ -179,32 +161,28 @@ def test_dynamic_array_index_keeps_variable_identity(tmp_path: Path, monkeypatch
|
|||||||
assert variable_name(tree.children[3].args[0]) is None
|
assert variable_name(tree.children[3].args[0]) is None
|
||||||
path = tmp_path / "dynamic.tcl"
|
path = tmp_path / "dynamic.tcl"
|
||||||
index = build_file_symbol_index(str(path), path.as_uri(), tree)
|
index = build_file_symbol_index(str(path), path.as_uri(), tree)
|
||||||
definition = next(
|
definition = next(item for item in index.occurrences if item.identity.name == "::custom_flag" and item.is_definition)
|
||||||
item for item in index.occurrences
|
|
||||||
if item.identity.name == "::custom_flag" and item.is_definition
|
|
||||||
)
|
|
||||||
assert definition.range.start.character == 4
|
assert definition.range.start.character == 4
|
||||||
assert definition.range.end.character == 15
|
assert definition.range.end.character == 15
|
||||||
assert definition.array_element is None
|
assert definition.array_element is None
|
||||||
assert any(item.identity.name == "::mom_path_name" for item in index.occurrences)
|
assert any(item.identity.name == "::mom_path_name" for item in index.occurrences)
|
||||||
highlighter = _Highlighter([], {})
|
highlighter = _Highlighter([], {})
|
||||||
tree.accept(highlighter, recurse=True)
|
tree.accept(highlighter, recurse=True)
|
||||||
assert any(
|
assert any(position == (0, 4) and length == 11 and kind == "variable" for position, length, kind, _ in highlighter._tokens)
|
||||||
position == (0, 4) and length == 11 and kind == "variable"
|
|
||||||
for position, length, kind, _ in highlighter._tokens
|
|
||||||
)
|
|
||||||
server, _, _ = _completion_server(tmp_path, monkeypatch)
|
server, _, _ = _completion_server(tmp_path, monkeypatch)
|
||||||
current = _document(path, source)
|
current = _document(path, source)
|
||||||
server.workspace.put_text_document(lsp.TextDocumentItem(
|
server.workspace.put_text_document(
|
||||||
uri=current.uri, language_id="tcl", version=1, text=source,
|
lsp.TextDocumentItem(
|
||||||
))
|
uri=current.uri,
|
||||||
|
language_id="tcl",
|
||||||
|
version=1,
|
||||||
|
text=source,
|
||||||
|
)
|
||||||
|
)
|
||||||
assert server.update_poco_completion_for_file(current)
|
assert server.update_poco_completion_for_file(current)
|
||||||
items = _complete(current, _position_after(source, "puts $custom"))
|
items = _complete(current, _position_after(source, "puts $custom"))
|
||||||
assert "custom_flag" in {item.label for item in items}
|
assert "custom_flag" in {item.label for item in items}
|
||||||
workspace_items = next(
|
workspace_items = next(items for item_path, items in server.completion_items_by_file_snapshot().items() if server.paths_equal(item_path, str(path)))
|
||||||
items for item_path, items in server.completion_items_by_file_snapshot().items()
|
|
||||||
if server.paths_equal(item_path, str(path))
|
|
||||||
)
|
|
||||||
assert {"quoted_flag", "command_flag"} <= {item.label for item in workspace_items}
|
assert {"quoted_flag", "command_flag"} <= {item.label for item in workspace_items}
|
||||||
|
|
||||||
|
|
||||||
@@ -236,18 +214,25 @@ def test_literal_array_components_complete_around_substitutions(tmp_path: Path,
|
|||||||
offset = marked.index("|")
|
offset = marked.index("|")
|
||||||
line = marked.replace("|", "")
|
line = marked.replace("|", "")
|
||||||
items = array_element_completions(
|
items = array_element_completions(
|
||||||
[line], lsp.Position(line=0, character=offset),
|
[line],
|
||||||
server.navigation_snapshot().values(), str(tmp_path / "caller.tcl"),
|
lsp.Position(line=0, character=offset),
|
||||||
|
server.navigation_snapshot().values(),
|
||||||
|
str(tmp_path / "caller.tcl"),
|
||||||
)
|
)
|
||||||
assert {item.label for item in items} == labels
|
assert {item.label for item in items} == labels
|
||||||
edit = next(item.text_edit for item in items if item.label == selected)
|
edit = next(item.text_edit for item in items if item.label == selected)
|
||||||
item = next(item for item in items if item.label == selected)
|
item = next(item for item in items if item.label == selected)
|
||||||
assert item.insert_text_format == lsp.InsertTextFormat.PlainText
|
assert item.insert_text_format == lsp.InsertTextFormat.PlainText
|
||||||
assert line[:edit.range.start.character] + edit.new_text + line[edit.range.end.character:] == expected
|
assert line[: edit.range.start.character] + edit.new_text + line[edit.range.end.character :] == expected
|
||||||
assert array_element_completions(
|
assert (
|
||||||
["set custom_flag(from_move,$::mom"], lsp.Position(line=0, character=31),
|
array_element_completions(
|
||||||
server.navigation_snapshot().values(), str(tmp_path / "caller.tcl"),
|
["set custom_flag(from_move,$::mom"],
|
||||||
) is None
|
lsp.Position(line=0, character=31),
|
||||||
|
server.navigation_snapshot().values(),
|
||||||
|
str(tmp_path / "caller.tcl"),
|
||||||
|
)
|
||||||
|
is None
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _argument_completion_request(source: str):
|
def _argument_completion_request(source: str):
|
||||||
@@ -277,9 +262,7 @@ def test_variable_completion_filters_and_ranks_candidates(tmp_path: Path, monkey
|
|||||||
other_builtin = standard_items.nx_variables[1]
|
other_builtin = standard_items.nx_variables[1]
|
||||||
assert declared_builtin.label in by_label
|
assert declared_builtin.label in by_label
|
||||||
assert other_builtin.label in by_label
|
assert other_builtin.label in by_label
|
||||||
assert by_label[declared_builtin.label].documentation == (
|
assert by_label[declared_builtin.label].documentation == (declared_builtin.documentation)
|
||||||
declared_builtin.documentation
|
|
||||||
)
|
|
||||||
assert by_label["localValue"].sort_text.startswith("000:")
|
assert by_label["localValue"].sort_text.startswith("000:")
|
||||||
assert by_label["globalValue"].sort_text.startswith("100:")
|
assert by_label["globalValue"].sort_text.startswith("100:")
|
||||||
assert by_label["workspaceValue"].sort_text.startswith("200:")
|
assert by_label["workspaceValue"].sort_text.startswith("200:")
|
||||||
@@ -307,18 +290,9 @@ def test_command_completion_filters_and_ranks_candidates(tmp_path: Path, monkeyp
|
|||||||
|
|
||||||
|
|
||||||
def test_completion_context_handles_nested_commands_and_utf16():
|
def test_completion_context_handles_nested_commands_and_utf16():
|
||||||
assert (
|
assert completion_context(["set result [work"], lsp.Position(line=0, character=16)) == CompletionContext.COMMAND
|
||||||
completion_context(["set result [work"], lsp.Position(line=0, character=16))
|
assert completion_context(["😀 puts $value"], lsp.Position(line=0, character=14)) == CompletionContext.VARIABLE
|
||||||
== CompletionContext.COMMAND
|
assert completion_context(["puts value"], lsp.Position(line=0, character=10)) == CompletionContext.GENERAL
|
||||||
)
|
|
||||||
assert (
|
|
||||||
completion_context(["😀 puts $value"], lsp.Position(line=0, character=14))
|
|
||||||
== CompletionContext.VARIABLE
|
|
||||||
)
|
|
||||||
assert (
|
|
||||||
completion_context(["puts value"], lsp.Position(line=0, character=10))
|
|
||||||
== CompletionContext.GENERAL
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_string_subcommands_and_compare_options_are_context_aware():
|
def test_string_subcommands_and_compare_options_are_context_aware():
|
||||||
@@ -330,9 +304,7 @@ def test_string_subcommands_and_compare_options_are_context_aware():
|
|||||||
"-length",
|
"-length",
|
||||||
"-nocase",
|
"-nocase",
|
||||||
}
|
}
|
||||||
assert _argument_completion_labels("string compare -nocase ") == {
|
assert _argument_completion_labels("string compare -nocase ") == {"-length"}
|
||||||
"-length"
|
|
||||||
}
|
|
||||||
assert _argument_completion_labels("string compare -length ") is None
|
assert _argument_completion_labels("string compare -length ") is None
|
||||||
|
|
||||||
|
|
||||||
@@ -356,22 +328,14 @@ def test_string_completion_inside_braced_conditions_and_bodies():
|
|||||||
subcommands = _argument_completion_labels(prefix + "[string ")
|
subcommands = _argument_completion_labels(prefix + "[string ")
|
||||||
assert subcommands is not None
|
assert subcommands is not None
|
||||||
assert {"compare", "equal", "is"} <= subcommands
|
assert {"compare", "equal", "is"} <= subcommands
|
||||||
assert _argument_completion_labels(prefix + "[string compare -") == {
|
assert _argument_completion_labels(prefix + "[string compare -") == {"-length", "-nocase"}
|
||||||
"-length", "-nocase"
|
assert _argument_completion_labels(prefix + "[string compare -nocase ") == {"-length"}
|
||||||
}
|
|
||||||
assert _argument_completion_labels(
|
|
||||||
prefix + "[string compare -nocase "
|
|
||||||
) == {"-length"}
|
|
||||||
|
|
||||||
|
|
||||||
def test_closed_braced_arguments_do_not_change_completion_context():
|
def test_closed_braced_arguments_do_not_change_completion_context():
|
||||||
assert _argument_completion_labels("puts {[string compare }") is None
|
assert _argument_completion_labels("puts {[string compare }") is None
|
||||||
assert _argument_completion_labels(
|
assert _argument_completion_labels("if {[string equal a b]} {string compare ") == {"-length", "-nocase"}
|
||||||
"if {[string equal a b]} {string compare "
|
assert _argument_completion_labels("if {[string equal a b] && [string is integer ") == {"-failindex", "-strict"}
|
||||||
) == {"-length", "-nocase"}
|
|
||||||
assert _argument_completion_labels(
|
|
||||||
"if {[string equal a b] && [string is integer "
|
|
||||||
) == {"-failindex", "-strict"}
|
|
||||||
|
|
||||||
|
|
||||||
def test_dict_array_namespace_file_and_info_subcommands():
|
def test_dict_array_namespace_file_and_info_subcommands():
|
||||||
@@ -413,9 +377,7 @@ def test_dict_array_namespace_file_and_info_subcommands():
|
|||||||
assert {"args", "body", "commands", "exists", "procs", "vars"} <= info_items
|
assert {"args", "body", "commands", "exists", "procs", "vars"} <= info_items
|
||||||
|
|
||||||
|
|
||||||
def test_variable_context_still_takes_priority_inside_tcl_command(
|
def test_variable_context_still_takes_priority_inside_tcl_command(tmp_path: Path, monkeypatch):
|
||||||
tmp_path: Path, monkeypatch
|
|
||||||
):
|
|
||||||
_, current, source = _completion_server(tmp_path, monkeypatch)
|
_, current, source = _completion_server(tmp_path, monkeypatch)
|
||||||
command_source = source.replace(
|
command_source = source.replace(
|
||||||
" puts $local\n",
|
" puts $local\n",
|
||||||
@@ -437,9 +399,7 @@ def test_variable_context_still_takes_priority_inside_tcl_command(
|
|||||||
assert "localValue" in {item.label for item in items}
|
assert "localValue" in {item.label for item in items}
|
||||||
|
|
||||||
|
|
||||||
def test_lsp_completion_returns_only_matching_command_options(
|
def test_lsp_completion_returns_only_matching_command_options(tmp_path: Path, monkeypatch):
|
||||||
tmp_path: Path, monkeypatch
|
|
||||||
):
|
|
||||||
_, current, _ = _completion_server(tmp_path, monkeypatch)
|
_, current, _ = _completion_server(tmp_path, monkeypatch)
|
||||||
source = "string compare "
|
source = "string compare "
|
||||||
current = _document(tmp_path / "current.tcl", source)
|
current = _document(tmp_path / "current.tcl", source)
|
||||||
@@ -459,9 +419,7 @@ def test_lsp_completion_returns_only_matching_command_options(
|
|||||||
assert all(item.sort_text.startswith("000:") for item in items)
|
assert all(item.sort_text.startswith("000:") for item in items)
|
||||||
|
|
||||||
|
|
||||||
def test_space_trigger_does_not_open_broad_fallback_completion(
|
def test_space_trigger_does_not_open_broad_fallback_completion(tmp_path: Path, monkeypatch):
|
||||||
tmp_path: Path, monkeypatch
|
|
||||||
):
|
|
||||||
_, current, _ = _completion_server(tmp_path, monkeypatch)
|
_, current, _ = _completion_server(tmp_path, monkeypatch)
|
||||||
source = "set value "
|
source = "set value "
|
||||||
current = _document(tmp_path / "current.tcl", source)
|
current = _document(tmp_path / "current.tcl", source)
|
||||||
@@ -550,9 +508,7 @@ def test_path_completion_is_relative_filtered_and_tcl_safe(tmp_path: Path):
|
|||||||
assert items[0].text_edit.range.start.character == len("source ")
|
assert items[0].text_edit.range.start.character == len("source ")
|
||||||
|
|
||||||
|
|
||||||
def test_lsp_source_completion_reads_paths_from_document_directory(
|
def test_lsp_source_completion_reads_paths_from_document_directory(tmp_path: Path, monkeypatch):
|
||||||
tmp_path: Path, monkeypatch
|
|
||||||
):
|
|
||||||
server, current, _ = _completion_server(tmp_path, monkeypatch)
|
server, current, _ = _completion_server(tmp_path, monkeypatch)
|
||||||
scripts = tmp_path / "scripts"
|
scripts = tmp_path / "scripts"
|
||||||
scripts.mkdir()
|
scripts.mkdir()
|
||||||
@@ -576,9 +532,7 @@ def test_lsp_source_completion_reads_paths_from_document_directory(
|
|||||||
assert "scripts/ignored.txt" not in labels
|
assert "scripts/ignored.txt" not in labels
|
||||||
|
|
||||||
|
|
||||||
def test_command_and_dict_for_snippets_use_lsp_snippet_placeholders(
|
def test_command_and_dict_for_snippets_use_lsp_snippet_placeholders(tmp_path: Path, monkeypatch):
|
||||||
tmp_path: Path, monkeypatch
|
|
||||||
):
|
|
||||||
_, current, source = _completion_server(tmp_path, monkeypatch)
|
_, current, source = _completion_server(tmp_path, monkeypatch)
|
||||||
command_items = _complete(current, _position_after(source, "localP", occurrence=1))
|
command_items = _complete(current, _position_after(source, "localP", occurrence=1))
|
||||||
command_by_label = {item.label: item for item in command_items}
|
command_by_label = {item.label: item for item in command_items}
|
||||||
@@ -595,14 +549,10 @@ def test_command_and_dict_for_snippets_use_lsp_snippet_placeholders(
|
|||||||
|
|
||||||
switch_arguments = _argument_completion_request("switch ")
|
switch_arguments = _argument_completion_request("switch ")
|
||||||
assert switch_arguments is not None
|
assert switch_arguments is not None
|
||||||
assert {"switch block", "-exact", "-glob", "-regexp"} <= {
|
assert {"switch block", "-exact", "-glob", "-regexp"} <= {item.label for item in switch_arguments.items}
|
||||||
item.label for item in switch_arguments.items
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_semantic_variable_and_procedure_argument_completion(
|
def test_semantic_variable_and_procedure_argument_completion(tmp_path: Path, monkeypatch):
|
||||||
tmp_path: Path, monkeypatch
|
|
||||||
):
|
|
||||||
_, current, source = _completion_server(tmp_path, monkeypatch)
|
_, current, source = _completion_server(tmp_path, monkeypatch)
|
||||||
|
|
||||||
variable_items = _complete(current, _position_after(source, " set "))
|
variable_items = _complete(current, _position_after(source, " set "))
|
||||||
@@ -633,9 +583,7 @@ def test_semantic_variable_and_procedure_argument_completion(
|
|||||||
assert "string" not in procedure_labels
|
assert "string" not in procedure_labels
|
||||||
|
|
||||||
|
|
||||||
def test_namespace_argument_completion_uses_navigation_index(
|
def test_namespace_argument_completion_uses_navigation_index(tmp_path: Path, monkeypatch):
|
||||||
tmp_path: Path, monkeypatch
|
|
||||||
):
|
|
||||||
server, current, _ = _completion_server(tmp_path, monkeypatch)
|
server, current, _ = _completion_server(tmp_path, monkeypatch)
|
||||||
namespace_source = "namespace eval tools { proc helper {} { return } }\n"
|
namespace_source = "namespace eval tools { proc helper {} { return } }\n"
|
||||||
namespace_document = _document(tmp_path / "namespaces.tcl", namespace_source)
|
namespace_document = _document(tmp_path / "namespaces.tcl", namespace_source)
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
"""Block templates and addresses reaching NX commands through variables and procs."""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import lsprotocol.types as lsp # type: ignore
|
||||||
|
from pygls.workspace import Workspace
|
||||||
|
|
||||||
|
import lsp_server
|
||||||
|
from lsp_tclserver import TclLanguageServer
|
||||||
|
from tools.def_flow import build_wrapper_table
|
||||||
|
|
||||||
|
PSC = """<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Configuration>
|
||||||
|
<Sourcing><Sequence>
|
||||||
|
<Layer Name="Service" SubFolder="service">
|
||||||
|
<DefinedEvents><Filename Name="service"/></DefinedEvents>
|
||||||
|
</Layer>
|
||||||
|
</Sequence></Sourcing>
|
||||||
|
</Configuration>
|
||||||
|
"""
|
||||||
|
|
||||||
|
DEF = """MACHINE Default
|
||||||
|
|
||||||
|
FORMATTING
|
||||||
|
{
|
||||||
|
ADDRESS SPOS
|
||||||
|
{
|
||||||
|
FORMAT Coordinate
|
||||||
|
}
|
||||||
|
BLOCK_TEMPLATE steady_rest
|
||||||
|
{
|
||||||
|
Text[M60]
|
||||||
|
}
|
||||||
|
BLOCK_TEMPLATE absolute_mode
|
||||||
|
{
|
||||||
|
Text[G90]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
LIBRARY = """proc LIB_call_cycle {cycle {prefix ""}} {
|
||||||
|
set block $cycle ; regsub -all "," $block "_" block
|
||||||
|
if {[catch {set line [MOM_do_template $block CREATE]} err]} {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
proc LIB_outer {mode name} {
|
||||||
|
LIB_call_cycle $name
|
||||||
|
}
|
||||||
|
proc LIB_force {address} {
|
||||||
|
MOM_force Once $address
|
||||||
|
}
|
||||||
|
proc LIB_log {message} {
|
||||||
|
puts $message
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
CALLER = """LIB_call_cycle "absolute_mode"
|
||||||
|
LIB_outer on steady_rest
|
||||||
|
LIB_force SPOS
|
||||||
|
LIB_log steady_rest
|
||||||
|
puts steady_rest
|
||||||
|
proc local {} {
|
||||||
|
set t "steady_rest"
|
||||||
|
MOM_do_template $t
|
||||||
|
set unused absolute_mode
|
||||||
|
foreach b {"steady_rest" absolute_mode} { MOM_do_template $b }
|
||||||
|
set l [list "absolute_mode"]
|
||||||
|
lappend l steady_rest
|
||||||
|
foreach x $l { LIB_call_cycle $x }
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _project(tmp_path: Path, monkeypatch):
|
||||||
|
(tmp_path / "service").mkdir()
|
||||||
|
(tmp_path / "post.psc").write_text(PSC, encoding="utf-8")
|
||||||
|
(tmp_path / "service" / "service.def").write_text(DEF, encoding="utf-8")
|
||||||
|
library = tmp_path / "library.tcl"
|
||||||
|
library.write_text(LIBRARY, encoding="utf-8")
|
||||||
|
caller = tmp_path / "caller.tcl"
|
||||||
|
caller.write_text(CALLER, encoding="utf-8")
|
||||||
|
|
||||||
|
server = TclLanguageServer(name="def-flow-test", version="1", max_workers=1)
|
||||||
|
server.protocol._workspace = Workspace( # pylint: disable=protected-access
|
||||||
|
root_uri=tmp_path.as_uri(),
|
||||||
|
sync_kind=lsp.TextDocumentSyncKind.Incremental,
|
||||||
|
workspace_folders=[lsp.WorkspaceFolder(uri=tmp_path.as_uri(), name="root")],
|
||||||
|
position_encoding=lsp.PositionEncodingKind.Utf16,
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(lsp_server, "LSP_SERVER", server)
|
||||||
|
server.refresh_psc_scripts([tmp_path])
|
||||||
|
for path, text in ((library, LIBRARY), (caller, CALLER)):
|
||||||
|
server.workspace.put_text_document(lsp.TextDocumentItem(uri=path.as_uri(), language_id="tcl", version=1, text=text))
|
||||||
|
server.update_poco_completion_for_file(server.workspace.get_text_document(path.as_uri()))
|
||||||
|
return server, caller
|
||||||
|
|
||||||
|
|
||||||
|
def _position(needle: str, occurrence: int = 0) -> lsp.Position:
|
||||||
|
index = -1
|
||||||
|
for _ in range(occurrence + 1):
|
||||||
|
index = CALLER.index(needle, index + 1)
|
||||||
|
line = CALLER.count("\n", 0, index)
|
||||||
|
return lsp.Position(line=line, character=index - (CALLER.rfind("\n", 0, index) + 1) + 1)
|
||||||
|
|
||||||
|
|
||||||
|
def _hover(caller: Path, needle: str, occurrence: int = 0):
|
||||||
|
return lsp_server.hover(
|
||||||
|
lsp.HoverParams(text_document=lsp.TextDocumentIdentifier(uri=caller.as_uri()), position=_position(needle, occurrence))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _definition(caller: Path, needle: str, occurrence: int = 0):
|
||||||
|
return lsp_server.goto_definition(
|
||||||
|
lsp.DefinitionParams(text_document=lsp.TextDocumentIdentifier(uri=caller.as_uri()), position=_position(needle, occurrence))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _hover_title(hover) -> str | None:
|
||||||
|
return hover and hover.contents.value.split("\n", 1)[0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_wrapper_table_follows_parameters_through_nested_procs(tmp_path, monkeypatch):
|
||||||
|
server, _ = _project(tmp_path, monkeypatch)
|
||||||
|
table = server.def_wrapper_table()
|
||||||
|
assert table["::LIB_call_cycle"] == {0: frozenset({"block_template"})}
|
||||||
|
assert table["::LIB_outer"] == {1: frozenset({"block_template"})}
|
||||||
|
assert table["::LIB_force"] == {0: frozenset({"address"})}
|
||||||
|
assert "::LIB_log" not in table
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_wrapper_table_stops_on_recursion():
|
||||||
|
flows = [
|
||||||
|
("::a", ((0, ("call", "::b", "::b", 0)),)),
|
||||||
|
("::b", ((0, ("call", "::a", "::a", 0)), (0, ("def", "address")))),
|
||||||
|
]
|
||||||
|
assert build_wrapper_table(flows) == {"::a": {0: frozenset({"address"})}, "::b": {0: frozenset({"address"})}}
|
||||||
|
|
||||||
|
|
||||||
|
def test_literal_argument_of_wrapper_proc_is_a_template(tmp_path, monkeypatch):
|
||||||
|
_, caller = _project(tmp_path, monkeypatch)
|
||||||
|
hover = _hover(caller, "absolute_mode")
|
||||||
|
assert _hover_title(hover).startswith("**Block template** `absolute_mode`")
|
||||||
|
assert (hover.range.start.line, hover.range.start.character, hover.range.end.character) == (0, 16, 29)
|
||||||
|
[location] = _definition(caller, "absolute_mode")
|
||||||
|
assert Path(location.uri).name == "service.def"
|
||||||
|
assert location.range.start.line == 12
|
||||||
|
|
||||||
|
|
||||||
|
def test_nested_wrapper_and_address_wrapper(tmp_path, monkeypatch):
|
||||||
|
_, caller = _project(tmp_path, monkeypatch)
|
||||||
|
assert _hover_title(_hover(caller, "steady_rest")).startswith("**Block template** `steady_rest`")
|
||||||
|
assert _hover_title(_hover(caller, "SPOS")).startswith("**Address** `SPOS`")
|
||||||
|
|
||||||
|
|
||||||
|
def test_same_name_without_flow_is_not_a_template(tmp_path, monkeypatch):
|
||||||
|
_, caller = _project(tmp_path, monkeypatch)
|
||||||
|
for needle, occurrence in (("steady_rest", 1), ("steady_rest", 2), ("absolute_mode", 1)):
|
||||||
|
assert _hover(caller, needle, occurrence) is None, (needle, occurrence)
|
||||||
|
assert _definition(caller, needle, occurrence) is None, (needle, occurrence)
|
||||||
|
|
||||||
|
|
||||||
|
def test_literals_flowing_through_local_variables(tmp_path, monkeypatch):
|
||||||
|
_, caller = _project(tmp_path, monkeypatch)
|
||||||
|
cases = (
|
||||||
|
("steady_rest", 3), # set t "steady_rest"; MOM_do_template $t
|
||||||
|
("steady_rest", 4), # foreach b {"steady_rest" ...}
|
||||||
|
("absolute_mode", 2), # foreach b {... absolute_mode}
|
||||||
|
("absolute_mode", 3), # set l [list "absolute_mode"]; foreach x $l { LIB_call_cycle $x }
|
||||||
|
("steady_rest", 5), # lappend l steady_rest
|
||||||
|
)
|
||||||
|
for needle, occurrence in cases:
|
||||||
|
assert _hover_title(_hover(caller, needle, occurrence)).startswith(f"**Block template** `{needle}`"), occurrence
|
||||||
|
assert _definition(caller, needle, occurrence), (needle, occurrence)
|
||||||
|
|
||||||
|
|
||||||
|
def test_derived_names_are_not_renamed(tmp_path, monkeypatch):
|
||||||
|
_, caller = _project(tmp_path, monkeypatch)
|
||||||
|
params = lsp.PrepareRenameParams(
|
||||||
|
text_document=lsp.TextDocumentIdentifier(uri=caller.as_uri()), position=_position("absolute_mode")
|
||||||
|
)
|
||||||
|
assert lsp_server.prepare_rename(params) is None
|
||||||
@@ -227,6 +227,25 @@ def test_block_list_shows_all_templates_quoted(tmp_path, monkeypatch):
|
|||||||
assert all(item.filter_text.startswith("BLOCK_LIST") for item in items)
|
assert all(item.filter_text.startswith("BLOCK_LIST") for item in items)
|
||||||
|
|
||||||
|
|
||||||
|
def test_block_list_item_resolves_to_template_preview(tmp_path, monkeypatch):
|
||||||
|
server, _ = _project(tmp_path, monkeypatch)
|
||||||
|
items = _complete(server, tmp_path, "set a 1\n BLOCK_LIST")
|
||||||
|
assert all(item.documentation is None for item in items)
|
||||||
|
item = next(item for item in items if item.label == "steady_rest")
|
||||||
|
resolved = lsp_server.on_completion_resolve(item)
|
||||||
|
assert resolved.documentation.kind == lsp.MarkupKind.Markdown
|
||||||
|
assert "**Block template** `steady_rest`" in resolved.documentation.value
|
||||||
|
assert "Text[M60]" in resolved.documentation.value
|
||||||
|
|
||||||
|
|
||||||
|
def test_address_list_item_resolves_to_address_table(tmp_path, monkeypatch):
|
||||||
|
server, _ = _project(tmp_path, monkeypatch)
|
||||||
|
items = _complete(server, tmp_path, "set a 1\n ADDR_LIST")
|
||||||
|
item = next(item for item in items if item.label == "SPOS")
|
||||||
|
resolved = lsp_server.on_completion_resolve(item)
|
||||||
|
assert "| Format | `Coordinate` |" in resolved.documentation.value
|
||||||
|
|
||||||
|
|
||||||
def test_block_list_ignores_variables_and_other_words(tmp_path, monkeypatch):
|
def test_block_list_ignores_variables_and_other_words(tmp_path, monkeypatch):
|
||||||
server, _ = _project(tmp_path, monkeypatch)
|
server, _ = _project(tmp_path, monkeypatch)
|
||||||
for source in ("set x $BLOCK_LIST", "set x MY_BLOCK_LIST", "set x steady"):
|
for source in ("set x $BLOCK_LIST", "set x MY_BLOCK_LIST", "set x steady"):
|
||||||
|
|||||||
Reference in New Issue
Block a user