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
202 lines
7.7 KiB
Python
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
|