feat(def_flow): recognize .def names propagated via variables and wrapper procs
Add a new def_flow analysis module that follows .def block template and address names through local variables and proc parameters, and a wrapper-table builder to resolve proc arguments that forward .def names. Derived names are resolved only for hover/definition (not for rename). Integrate this into the LSP: - lsp_server: add _word_at and _tcl_def_symbol helpers; fallback to derived_def_symbol when direct NX-argument navigation fails for hover, goto-definition and references; return proper ranges for hover. - lsp_tclserver: cache and expose a def_wrapper_table built from index def_flows (with cache invalidation on index generation). Also add unit tests for def_flow and update CHANGELOG to note hover/ definition and completion preview improvements for derived names.
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user