Files
nx_post_support/server/src/tools/def_navigation.py
T
Christoph c8466d2e32 feat(def-navigation): surface last-loaded .def declaration for hover/GoTo
When a block template or address is declared in multiple PSC layers, hover and
Go to Definition now resolve to the effective declaration (the one from the
last-loaded .def file). Find References and Rename still include all
declarations.

- add effective_def_locations(documents, target, uris) helper (returns the last declaration)
- use effective_def_locations in lsp_server for goto/definition endpoints
- change def_hover_markdown to show the effective declaration and append a short
  "_Overrides ..._" note listing overridden declarations
- update changelog and add a test that verifies hover/GoTo reference the last
  loaded declaration while references include all declarations
2026-09-25 21:38:06 +02:00

202 lines
7.7 KiB
Python

"""Navigation between Tcl code and the block templates and addresses of .def files."""
from __future__ import annotations
from pathlib import Path
import lsprotocol.types as lsp
from tools.def_symbols import ADDRESS, BLOCK_TEMPLATE, FORMAT, DefDeclaration, DefDocument
from tools.navigation import FileSymbolIndex, SymbolOccurrence
DefTarget = tuple[str, str]
_KIND_LABELS = {BLOCK_TEMPLATE: "Block template", ADDRESS: "Address", FORMAT: "Format"}
# Address properties in display order; others follow as declared.
_ADDRESS_PROPERTIES = (
("FORMAT", "Format"),
("LEADER", "Leader"),
("TRAILER", "Trailer"),
("MIN", "Min"),
("MAX", "Max"),
("FORCE", "Modality"),
("ZERO_FORMAT", "Zero format"),
("INCREMENTAL", "Incremental"),
("OMIT", "Omit"),
)
_MODALITY = {
"OFF": "modal, output only on change",
"ONCE": "output once, then modal",
"ALWAYS": "non-modal, always output",
}
def _range(line: int, start: int, end: int) -> lsp.Range:
return lsp.Range(
start=lsp.Position(line=line, character=start),
end=lsp.Position(line=line, character=end),
)
def _contains(line: int, start: int, end: int, position: lsp.Position) -> bool:
return position.line == line and start <= position.character <= end
def def_symbol_at(document: DefDocument, position: lsp.Position) -> tuple[DefTarget, lsp.Range, bool] | None:
"""Return the block template or address at ``position`` of a .def document.
The flag tells whether the position is on a declaration.
"""
for declaration in document.declarations:
if declaration.kind != FORMAT and _contains(declaration.line, declaration.start, declaration.end, position):
return (declaration.kind, declaration.name), _range(declaration.line, declaration.start, declaration.end), True
for reference in document.references:
if _contains(reference.line, reference.start, reference.end, position):
return (reference.kind, reference.name), _range(reference.line, reference.start, reference.end), False
return None
def _uri(path: str, uris: dict[str, str]) -> str:
return uris.get(path) or Path(path).as_uri()
def def_declarations(documents: dict[str, DefDocument], target: DefTarget) -> list[tuple[str, DefDeclaration]]:
kind, name = target
return [
(path, declaration)
for path, document in documents.items()
for declaration in document.declarations
if declaration.kind == kind and declaration.name == name
]
def def_definition_locations(
documents: dict[str, DefDocument], target: DefTarget, uris: dict[str, str] | None = None
) -> list[lsp.Location]:
uris = uris or {}
return [
lsp.Location(uri=_uri(path, uris), range=_range(declaration.line, declaration.start, declaration.end))
for path, declaration in def_declarations(documents, target)
]
def effective_def_locations(
documents: dict[str, DefDocument], target: DefTarget, uris: dict[str, str] | None = None
) -> list[lsp.Location]:
"""The declaration NX uses: ``documents`` follow the PSC layer order, the last one wins."""
return def_definition_locations(documents, target, uris)[-1:]
def def_reference_locations(
documents: dict[str, DefDocument],
target: DefTarget,
include_declaration: bool,
uris: dict[str, str] | None = None,
) -> list[lsp.Location]:
"""Return .def occurrences: declarations and addresses used in block templates."""
kind, name = target
uris = uris or {}
locations = def_definition_locations(documents, target, uris) if include_declaration else []
for path, document in documents.items():
for reference in document.references:
if reference.kind == kind and reference.name == name:
locations.append(
lsp.Location(uri=_uri(path, uris), range=_range(reference.line, reference.start, reference.end))
)
return locations
def tcl_def_occurrences(
indexes: dict[str, FileSymbolIndex], target: DefTarget
) -> list[tuple[FileSymbolIndex, SymbolOccurrence]]:
kind, name = target
return [
(index, occurrence)
for index in indexes.values()
for occurrence in index.occurrences
if occurrence.identity.kind == kind and occurrence.identity.name == name
]
def all_def_target_locations(
documents: dict[str, DefDocument],
indexes: dict[str, FileSymbolIndex],
target: DefTarget,
include_declaration: bool,
uris: dict[str, str] | None = None,
) -> list[lsp.Location]:
locations = def_reference_locations(documents, target, include_declaration, uris)
locations.extend(
lsp.Location(uri=index.uri, range=occurrence.range) for index, occurrence in tcl_def_occurrences(indexes, target)
)
unique = {}
for location in locations:
key = (location.uri, location.range.start.line, location.range.start.character)
unique.setdefault(key, location)
return sorted(unique.values(), key=lambda item: (item.uri, item.range.start.line, item.range.start.character))
def def_rename_edits(
documents: dict[str, DefDocument],
indexes: dict[str, FileSymbolIndex],
target: DefTarget,
new_name: str,
uris: dict[str, str] | None = None,
) -> lsp.WorkspaceEdit | None:
"""Rename a block template or address in all .def and Tcl files, if it is declared."""
if not def_declarations(documents, target):
return None
changes: dict[str, list[lsp.TextEdit]] = {}
for location in all_def_target_locations(documents, indexes, target, True, uris):
changes.setdefault(location.uri, []).append(lsp.TextEdit(range=location.range, new_text=new_name))
for edits in changes.values():
edits.sort(key=lambda edit: (edit.range.start.line, edit.range.start.character), reverse=True)
return lsp.WorkspaceEdit(changes=changes)
def _escape_cell(value: str) -> str:
return value.replace("|", "\\|") or " "
def _address_table(declaration: DefDeclaration, formats: dict[str, DefDeclaration]) -> str:
properties = dict(declaration.properties)
rows = []
for key, label in _ADDRESS_PROPERTIES:
value = properties.pop(key, None)
if value is None:
continue
cell = f"`{value}`" if value else ""
if key == "FORMAT" and value in formats:
cell += f" → `{dict(formats[value].properties).get('FORMAT', '')}`"
if key == "FORCE":
cell += f" ({_MODALITY[value.upper()]})" if value.upper() in _MODALITY else ""
rows.append(f"| {label} | {_escape_cell(cell)} |")
rows.extend(f"| {key.title()} | {_escape_cell(f'`{value}`' if value else '')} |" for key, value in properties.items())
if not rows:
return "_No properties_"
return "\n".join(["| Property | Value |", "|---|---|", *rows])
def def_hover_markdown(documents: dict[str, DefDocument], target: DefTarget) -> str | None:
"""Describe the effective (last loaded) declaration and name the ones it overrides."""
declarations = def_declarations(documents, target)
if not declarations:
return None
formats = {
declaration.name: declaration
for document in documents.values()
for declaration in document.declarations
if declaration.kind == FORMAT
}
kind, name = target
path, declaration = declarations[-1]
header = f"**{_KIND_LABELS[kind]}** `{name}` — {Path(path).name}:{declaration.line + 1}"
if kind == ADDRESS:
body = _address_table(declaration, formats)
else:
body = f"```def\n{declaration.text}\n```"
markdown = f"{header}\n\n{body}"
if len(declarations) > 1:
overridden = ", ".join(f"{Path(other).name}:{item.line + 1}" for other, item in declarations[:-1])
markdown += f"\n\n---\n\n_Overrides {overridden}_"
return markdown