feat(def): add cross-file navigation and rename for PSC .def block templates and addresses

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.
This commit is contained in:
2026-09-24 22:55:40 +02:00
parent 1d36b3bb77
commit 513b1cb340
8 changed files with 473 additions and 28 deletions
+21 -14
View File
@@ -18,7 +18,7 @@ from tools import checks, incremental_parse, parser
from tools.completion_items import CompletionCollector
from tools.tcloo_symbols import class_completion_items
from tools.tcloo_completion import indexed_classes
from tools.def_symbols import DefSymbols, read_def_symbols
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.formatter import NxFormatter as Formatter
from tools.index_cache import FileStat, IndexCache, file_stat
@@ -62,8 +62,8 @@ class TclLanguageServer(LanguageServer):
self.psc_script_paths: list[str] = []
self._psc_files: dict[str, list[pathlib.Path]] = {}
self._psc_lock = threading.RLock()
# .def path -> BLOCK_TEMPLATE/ADDRESS names, in PSC DefinedEvents order.
self.def_symbols: dict[str, DefSymbols] = {}
# .def path -> parsed declarations, in PSC DefinedEvents order.
self.def_documents: dict[str, DefDocument] = {}
self.navigation_indexes: dict[str, FileSymbolIndex] = {}
self.variable_indexes: dict[
str,
@@ -257,7 +257,7 @@ class TclLanguageServer(LanguageServer):
def refresh_def_symbols(self, roots, report=LOGGER.warning):
"""Read the block templates and addresses of all .def files listed as PSC DefinedEvents."""
symbols: dict[str, DefSymbols] = {}
documents: dict[str, DefDocument] = {}
for root in roots:
for psc in get_all_psc_files(root):
try:
@@ -266,33 +266,40 @@ class TclLanguageServer(LanguageServer):
report(f"Could not read PSC {psc}: {error}")
continue
for def_file in def_files:
if str(def_file) in symbols:
if str(def_file) in documents:
continue
try:
symbols[str(def_file)] = read_def_symbols(def_file)
documents[str(def_file)] = parse_def_document(read_def_source(def_file))
except OSError as error:
report(f"Could not read DEF file {def_file}: {error}")
with self._index_lock:
self.def_symbols = symbols
self.def_documents = documents
def _def_symbol_items(self, attribute: str, kind, description: str) -> list[lsp.CompletionItem]:
def def_documents_snapshot(self, current_path=None, current_source: str | None = None) -> dict[str, DefDocument]:
"""Return the PSC .def documents; ``current_source`` replaces the file being edited."""
with self._index_lock:
symbols = dict(self.def_symbols)
documents = dict(self.def_documents)
if current_path is not None and current_source is not None:
key = next((path for path in documents if self.paths_equal(path, current_path)), os.fspath(current_path))
documents[key] = parse_def_document(current_source)
return documents
def _def_symbol_items(self, kind: str, item_kind, description: str) -> list[lsp.CompletionItem]:
return [
lsp.CompletionItem(
label=name,
kind=kind,
kind=item_kind,
detail=f"{description} ({pathlib.Path(path).name})",
)
for path, def_symbols in symbols.items()
for name in getattr(def_symbols, attribute)
for path, document in self.def_documents_snapshot().items()
for name in document.names(kind)
]
def block_template_items(self) -> list[lsp.CompletionItem]:
return self._def_symbol_items("block_templates", lsp.CompletionItemKind.Struct, "Block template")
return self._def_symbol_items(BLOCK_TEMPLATE, lsp.CompletionItemKind.Struct, "Block template")
def address_items(self) -> list[lsp.CompletionItem]:
return self._def_symbol_items("addresses", lsp.CompletionItemKind.Field, "Address")
return self._def_symbol_items(ADDRESS, lsp.CompletionItemKind.Field, "Address")
def refresh_psc_scripts(self, roots, report=LOGGER.warning):
"""Index PSC dependencies through the same pipeline as workspace procs."""