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
+125 -2
View File
@@ -52,6 +52,15 @@ from tools.completion_items import (
completion_context,
ranked_completion_items,
)
from tools.def_navigation import (
all_def_target_locations,
def_declarations,
def_definition_locations,
def_hover_markdown,
def_rename_edits,
def_symbol_at,
)
from tools.def_symbols import parse_def_document
from tools.folding_ranges import build_folding_ranges
from tools.index_cache import IndexCache
from tools.inlay_hint import (
@@ -59,6 +68,7 @@ from tools.inlay_hint import (
build_builtin_inlay_signatures,
)
from tools.navigation import (
DEF_SYMBOL_KINDS,
SymbolIdentity,
call_hierarchy_identity,
call_hierarchy_items,
@@ -295,6 +305,7 @@ SYMBOL_LIST_KEYWORDS = {
"BLOCK_LIST": (lambda: LSP_SERVER.block_template_items(), "block templates"),
"ADDR_LIST": (lambda: LSP_SERVER.address_items(), "addresses"),
}
DEF_NAME_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
_WORD_BEFORE_CURSOR_RE = re.compile(r"(?<![$\w])[A-Za-z_]+$")
@@ -709,6 +720,10 @@ def hover(params: lsp.HoverParams) -> lsp.Hover:
else:
return None
def_hover = _tcl_def_hover(document_uri, pos, token)
if def_hover is not None:
return def_hover
# 1) If token is a known MOM proc/variable, return built-in hover
match = BUILTIN_HOVER_ITEMS.get(token)
if match and match.get("kind") == "function":
@@ -748,6 +763,22 @@ def hover(params: lsp.HoverParams) -> lsp.Hover:
return None
def _def_target(identity: SymbolIdentity) -> tuple[str, str] | None:
return (identity.kind, identity.name) if identity.kind in DEF_SYMBOL_KINDS else None
def _tcl_def_hover(uri: str, position: lsp.Position, token: str) -> lsp.Hover | None:
documents = LSP_SERVER.def_documents_snapshot()
if not any(token in document.names(kind) for document in documents.values() for kind in DEF_SYMBOL_KINDS):
return None
context = _navigation_context(uri, position)
target = context and _def_target(context[3])
markdown = target and def_hover_markdown(documents, target)
if not markdown:
return None
return lsp.Hover(lsp.MarkupContent(kind=lsp.MarkupKind.Markdown, value=markdown), range=context[2].range)
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DEFINITION)
def goto_definition(params: lsp.DefinitionParams):
"""Resolve TclOO declarations, then indexed proc and variable definitions."""
@@ -770,6 +801,9 @@ def goto_definition(params: lsp.DefinitionParams):
return None
indexes, definitions, _, identity = context
target = _def_target(identity)
if target is not None:
return _sorted_locations(def_definition_locations(LSP_SERVER.def_documents_snapshot(), target)) or None
locations = [lsp.Location(uri=index.uri, range=occurrence.range) for index, occurrence in matching_occurrences(identity, indexes, definitions) if occurrence.is_definition]
return _sorted_locations(locations) or None
@@ -811,6 +845,11 @@ def references(params: lsp.ReferenceParams) -> list[lsp.Location]:
return []
indexes, definitions, _, identity = context
target = _def_target(identity)
if target is not None:
return all_def_target_locations(
LSP_SERVER.def_documents_snapshot(), indexes, target, params.context.include_declaration
)
locations = [
lsp.Location(uri=index.uri, range=occurrence.range)
for index, occurrence in matching_occurrences(identity, indexes, definitions)
@@ -858,7 +897,11 @@ def prepare_rename(params: lsp.PrepareRenameParams):
return None
indexes, definitions, occurrence, identity = context
if not _is_renamable(identity, indexes, definitions):
target = _def_target(identity)
if target is not None:
if not def_declarations(LSP_SERVER.def_documents_snapshot(), target):
return None
elif not _is_renamable(identity, indexes, definitions):
return None
return lsp.PrepareRenamePlaceholder(range=occurrence.range, placeholder=occurrence.placeholder)
@@ -868,7 +911,7 @@ def prepare_rename(params: lsp.PrepareRenameParams):
lsp.RenameOptions(prepare_provider=True),
)
def rename(params: lsp.RenameParams) -> lsp.WorkspaceEdit | None:
if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", params.new_name):
if not DEF_NAME_RE.fullmatch(params.new_name):
return None
context = _navigation_context(params.text_document.uri, params.position)
@@ -876,6 +919,9 @@ def rename(params: lsp.RenameParams) -> lsp.WorkspaceEdit | None:
return None
indexes, definitions, _, identity = context
target = _def_target(identity)
if target is not None:
return def_rename_edits(LSP_SERVER.def_documents_snapshot(), indexes, target, params.new_name)
if not _is_renamable(identity, indexes, definitions):
return None
@@ -905,6 +951,83 @@ def rename(params: lsp.RenameParams) -> lsp.WorkspaceEdit | None:
return lsp.WorkspaceEdit(changes=changes)
# .def files are not synchronized with the server; the client sends their
# current text with each request.
DEF_REQUEST_DEFINITION = "nxPostSupport/def/definition"
DEF_REQUEST_HOVER = "nxPostSupport/def/hover"
DEF_REQUEST_REFERENCES = "nxPostSupport/def/references"
DEF_REQUEST_PREPARE_RENAME = "nxPostSupport/def/prepareRename"
DEF_REQUEST_RENAME = "nxPostSupport/def/rename"
def _def_request_context(params):
uri = params.textDocument.uri
position = lsp.Position(line=params.position.line, character=params.position.character)
path = uris.to_fs_path(uri)
documents = LSP_SERVER.def_documents_snapshot(path, params.text)
symbol = def_symbol_at(parse_def_document(params.text), position)
if symbol is None:
return None
target, range_, _ = symbol
current = next(key for key in documents if LSP_SERVER.paths_equal(key, path))
return documents, {current: uri}, target, range_
@LSP_SERVER.feature(DEF_REQUEST_DEFINITION)
def def_definition(params) -> list[lsp.Location] | None:
context = _def_request_context(params)
if context is None:
return None
documents, def_uris, target, _ = context
return _sorted_locations(def_definition_locations(documents, target, def_uris)) or None
@LSP_SERVER.feature(DEF_REQUEST_HOVER)
def def_hover(params) -> lsp.Hover | None:
context = _def_request_context(params)
if context is None:
return None
documents, _, target, range_ = context
markdown = def_hover_markdown(documents, target)
if markdown is None:
return None
return lsp.Hover(lsp.MarkupContent(kind=lsp.MarkupKind.Markdown, value=markdown), range=range_)
@LSP_SERVER.feature(DEF_REQUEST_REFERENCES)
def def_references(params) -> list[lsp.Location]:
context = _def_request_context(params)
if context is None:
return []
documents, def_uris, target, _ = context
include_declaration = bool(getattr(params, "includeDeclaration", True))
indexes, _ = LSP_SERVER.navigation_state()
return all_def_target_locations(documents, indexes, target, include_declaration, def_uris)
@LSP_SERVER.feature(DEF_REQUEST_PREPARE_RENAME)
def def_prepare_rename(params):
context = _def_request_context(params)
if context is None:
return None
documents, _, target, range_ = context
if not def_declarations(documents, target):
return None
return lsp.PrepareRenamePlaceholder(range=range_, placeholder=target[1])
@LSP_SERVER.feature(DEF_REQUEST_RENAME)
def def_rename(params) -> lsp.WorkspaceEdit | None:
if not DEF_NAME_RE.fullmatch(params.newName):
return None
context = _def_request_context(params)
if context is None:
return None
documents, def_uris, target, _ = context
indexes, _ = LSP_SERVER.navigation_state()
return def_rename_edits(documents, indexes, target, params.newName, def_uris)
@LSP_SERVER.feature(lsp.WORKSPACE_SYMBOL)
def workspace_symbol(params: lsp.WorkspaceSymbolParams):
return workspace_symbols(LSP_SERVER.navigation_snapshot(), params.query)