Add client and server support to navigate, inspect, reference, and rename block template and address symbols declared in PSC .def files: - Client: register language providers for .def (definition, hover, references, prepare/provide rename) and send the current document text with each request. - Server: parse .def files into DefDocument/DefDeclaration/DefReference, expose def-specific LSP endpoints (definition/hover/references/prepareRename/rename), and integrate .def lookups into existing Tcl hover/definition/references/rename flows so Tcl calls jump to .def declarations. - Tcl server keeps a snapshot API for .def documents (current editor content can replace file on request); only declared names in loaded .def files can be renamed. Name validation uses DEF_NAME_RE. Update README and CHANGELOG to document navigation features.
181 lines
5.6 KiB
Python
181 lines
5.6 KiB
Python
"""Block templates, addresses and formats declared in NX post definition (.def) files."""
|
|
|
|
import re
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
|
|
BLOCK_TEMPLATE = "block_template"
|
|
ADDRESS = "address"
|
|
FORMAT = "format"
|
|
|
|
_KINDS = {"BLOCK_TEMPLATE": BLOCK_TEMPLATE, "ADDRESS": ADDRESS, "FORMAT": FORMAT}
|
|
_HEADER_RE = re.compile(r"^\s*(BLOCK_TEMPLATE|ADDRESS|FORMAT)\s+([^\s{]+)")
|
|
# A block template element is an address followed by its expression: X[$mom_pos(0)].
|
|
_ELEMENT_RE = re.compile(r"^\s*([A-Za-z_]\w*)\[")
|
|
_PROPERTY_RE = re.compile(r"^\s*([A-Za-z_]+)\s*(.*?)\s*$")
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class DefDeclaration:
|
|
kind: str
|
|
name: str
|
|
# 0-based line and UTF-16 columns of the name.
|
|
line: int
|
|
start: int
|
|
end: int
|
|
end_line: int
|
|
text: str
|
|
# ADDRESS: body properties such as ("LEADER", '"X"'); FORMAT: (("FORMAT", '"%d"'),).
|
|
properties: tuple[tuple[str, str], ...] = ()
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class DefReference:
|
|
"""An address used as element of a block template."""
|
|
|
|
kind: str
|
|
name: str
|
|
line: int
|
|
start: int
|
|
end: int
|
|
container: str
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class DefDocument:
|
|
declarations: tuple[DefDeclaration, ...] = ()
|
|
references: tuple[DefReference, ...] = ()
|
|
|
|
def names(self, kind: str) -> tuple[str, ...]:
|
|
return tuple(dict.fromkeys(item.name for item in self.declarations if item.kind == kind))
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class DefSymbols:
|
|
block_templates: tuple[str, ...] = ()
|
|
addresses: tuple[str, ...] = ()
|
|
|
|
|
|
def _utf16_length(text: str) -> int:
|
|
return len(text.encode("utf-16-le")) // 2
|
|
|
|
|
|
def _is_comment(line: str) -> bool:
|
|
return line.lstrip().startswith("#")
|
|
|
|
|
|
def _body(lines: list[str], header: int, rest: str) -> tuple[int, int, int] | None:
|
|
"""Return (first body line, first body column, closing line) of a braced body.
|
|
|
|
The body starts after the first "{" on the header line or on a following
|
|
line and ends at the next line starting with "}".
|
|
"""
|
|
if "{" in rest:
|
|
after = rest.split("{", 1)[1]
|
|
if "}" in after:
|
|
return None
|
|
start = header + 1
|
|
else:
|
|
start = header + 1
|
|
while start < len(lines) and (not lines[start].strip() or _is_comment(lines[start])):
|
|
start += 1
|
|
if start >= len(lines) or not lines[start].lstrip().startswith("{"):
|
|
return None
|
|
start += 1
|
|
end = start
|
|
while end < len(lines) and not lines[end].lstrip().startswith("}"):
|
|
end += 1
|
|
return start, 0, min(end, len(lines) - 1)
|
|
|
|
|
|
def parse_def_document(source: str) -> DefDocument:
|
|
"""Parse the block templates, addresses and formats of a .def source."""
|
|
lines = source.splitlines()
|
|
declarations: list[DefDeclaration] = []
|
|
references: list[DefReference] = []
|
|
index = 0
|
|
while index < len(lines):
|
|
line = lines[index]
|
|
match = None if _is_comment(line) else _HEADER_RE.match(line)
|
|
if match is None:
|
|
index += 1
|
|
continue
|
|
|
|
kind = _KINDS[match.group(1)]
|
|
name = match.group(2)
|
|
start = _utf16_length(line[: match.start(2)])
|
|
end = start + _utf16_length(name)
|
|
rest = line[match.end(2) :]
|
|
|
|
if kind == FORMAT:
|
|
declarations.append(
|
|
DefDeclaration(kind, name, index, start, end, index, line.strip(), (("FORMAT", rest.strip()),))
|
|
)
|
|
index += 1
|
|
continue
|
|
|
|
body = _body(lines, index, rest)
|
|
end_line = index if body is None else body[2]
|
|
properties: list[tuple[str, str]] = []
|
|
if body is not None:
|
|
for number in range(body[0], body[2]):
|
|
body_line = lines[number]
|
|
if _is_comment(body_line) or not body_line.strip():
|
|
continue
|
|
if kind == BLOCK_TEMPLATE:
|
|
element = _ELEMENT_RE.match(body_line)
|
|
if element is not None:
|
|
element_start = _utf16_length(body_line[: element.start(1)])
|
|
references.append(
|
|
DefReference(
|
|
ADDRESS,
|
|
element.group(1),
|
|
number,
|
|
element_start,
|
|
element_start + _utf16_length(element.group(1)),
|
|
name,
|
|
)
|
|
)
|
|
else:
|
|
prop = _PROPERTY_RE.match(body_line)
|
|
if prop is not None:
|
|
properties.append((prop.group(1).upper(), prop.group(2)))
|
|
|
|
declarations.append(
|
|
DefDeclaration(
|
|
kind,
|
|
name,
|
|
index,
|
|
start,
|
|
end,
|
|
end_line,
|
|
"\n".join(lines[index : end_line + 1]),
|
|
tuple(properties),
|
|
)
|
|
)
|
|
index = end_line + 1
|
|
|
|
return DefDocument(tuple(declarations), tuple(references))
|
|
|
|
|
|
def parse_def_symbols(source: str) -> DefSymbols:
|
|
"""Return the BLOCK_TEMPLATE and ADDRESS names of a .def source in declaration order."""
|
|
document = parse_def_document(source)
|
|
return DefSymbols(
|
|
block_templates=document.names(BLOCK_TEMPLATE),
|
|
addresses=document.names(ADDRESS),
|
|
)
|
|
|
|
|
|
def read_def_source(path: Path) -> str:
|
|
data = path.read_bytes()
|
|
try:
|
|
return data.decode("utf-8-sig")
|
|
except UnicodeDecodeError:
|
|
# Older Windows NX layers use the ANSI code page.
|
|
return data.decode("cp1252")
|
|
|
|
|
|
def read_def_symbols(path: Path) -> DefSymbols:
|
|
return parse_def_symbols(read_def_source(path))
|