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:
+125
-2
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user