From c8466d2e32ed79de2f8b876361145795ead5e55f Mon Sep 17 00:00:00 2001 From: Christoph Brandau Date: Fri, 25 Sep 2026 21:38:06 +0200 Subject: [PATCH] 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 --- CHANGELOG.md | 4 ++ server/src/lsp_server.py | 8 ++-- server/src/tools/def_navigation.py | 28 +++++++---- .../tests/python_tests/test_def_navigation.py | 47 +++++++++++++++++++ 4 files changed, 74 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88a259b..90e6f75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,10 @@ Versions correspond to the Git tags of this repository. - Warning for block template and address names in NX commands (`MOM_do_template stedy_rest`) that no loaded `.def` file declares; names built at runtime (`$var`, `"CYCLE_$x"`) are not checked, and the warnings update when a `.def` file changes - Completion items for block templates and addresses (`BLOCK_LIST`, `ADDR_LIST`, `MOM_do_template`, ...) show the same preview as the hover +### Changed + +- When a block template or address is declared in several PSC layers, hover and Go to Definition show only the effective one, the declaration of the last loaded `.def` file, and name the declarations it overrides; Find References and Rename still include all declarations + ### Documentation - Document DEF block templates, addresses, `BLOCK_LIST`/`ADDR_LIST`, and the formatting changes in the README diff --git a/server/src/lsp_server.py b/server/src/lsp_server.py index 9db0875..eed6398 100644 --- a/server/src/lsp_server.py +++ b/server/src/lsp_server.py @@ -55,8 +55,8 @@ from tools.completion_items import ( from tools.def_navigation import ( all_def_target_locations, def_declarations, - def_definition_locations, def_hover_markdown, + effective_def_locations, def_rename_edits, def_symbol_at, ) @@ -848,12 +848,12 @@ def goto_definition(params: lsp.DefinitionParams): context = _navigation_context(params.text_document.uri, params.position) if context is None: symbol = _tcl_def_symbol(params.text_document.uri, params.position, _word_at(params.text_document.uri, params.position)) - return symbol and _sorted_locations(def_definition_locations(symbol[0], symbol[1])) or None + return symbol and effective_def_locations(symbol[0], symbol[1]) or 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 + return effective_def_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 @@ -1033,7 +1033,7 @@ def def_definition(params) -> list[lsp.Location] | None: if context is None: return None documents, def_uris, target, _ = context - return _sorted_locations(def_definition_locations(documents, target, def_uris)) or None + return effective_def_locations(documents, target, def_uris) or None @LSP_SERVER.feature(DEF_REQUEST_HOVER) diff --git a/server/src/tools/def_navigation.py b/server/src/tools/def_navigation.py index b04bb45..c1e134a 100644 --- a/server/src/tools/def_navigation.py +++ b/server/src/tools/def_navigation.py @@ -79,6 +79,13 @@ def def_definition_locations( ] +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, @@ -170,6 +177,7 @@ def _address_table(declaration: DefDeclaration, formats: dict[str, DefDeclaratio 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 @@ -180,12 +188,14 @@ def def_hover_markdown(documents: dict[str, DefDocument], target: DefTarget) -> if declaration.kind == FORMAT } kind, name = target - sections = [] - for path, declaration in declarations: - 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```" - sections.append(f"{header}\n\n{body}") - return "\n\n---\n\n".join(sections) + 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 diff --git a/server/tests/python_tests/test_def_navigation.py b/server/tests/python_tests/test_def_navigation.py index 4ac219a..b86088d 100644 --- a/server/tests/python_tests/test_def_navigation.py +++ b/server/tests/python_tests/test_def_navigation.py @@ -240,3 +240,50 @@ def test_def_rename_updates_tcl_callers(tmp_path, monkeypatch): edits = {Path(uri).name: [(e.range.start.line, e.range.start.character) for e in items] for uri, items in edit.changes.items()} assert edits == {"caller.tcl": [(1, 20)], "service.def": [(14, 19)]} assert lsp_server.def_rename(_def_params(def_file, "steady_rest", newName="bad name")) is None + + +LAYERED_PSC = """ + + + + + + +""" + + +def test_template_in_several_layers_shows_the_last_loaded_one(tmp_path, monkeypatch): + for folder, text in (("ctrl", "Text[M17]"), ("oem", "Text[RET]")): + (tmp_path / folder).mkdir() + (tmp_path / folder / f"{folder}.def").write_text( + f"MACHINE X\n\nFORMATTING\n{{\n BLOCK_TEMPLATE end_of_subprogram\n {{\n {text}\n }}\n}}\n", encoding="utf-8" + ) + (tmp_path / "post.psc").write_text(LAYERED_PSC, encoding="utf-8") + source = "MOM_do_template end_of_subprogram\n" + caller = tmp_path / "caller.tcl" + caller.write_text(source, encoding="utf-8") + server = TclLanguageServer(name="def-layer-test", version="1", max_workers=1) + server.protocol._workspace = Workspace( # pylint: disable=protected-access + root_uri=tmp_path.as_uri(), + sync_kind=lsp.TextDocumentSyncKind.Incremental, + workspace_folders=[lsp.WorkspaceFolder(uri=tmp_path.as_uri(), name="root")], + position_encoding=lsp.PositionEncodingKind.Utf16, + ) + monkeypatch.setattr(lsp_server, "LSP_SERVER", server) + server.refresh_psc_scripts([tmp_path]) + server.workspace.put_text_document(lsp.TextDocumentItem(uri=caller.as_uri(), language_id="tcl", version=1, text=source)) + server.update_poco_completion_for_file(server.workspace.get_text_document(caller.as_uri())) + document = lsp.TextDocumentIdentifier(uri=caller.as_uri()) + position = lsp.Position(line=0, character=20) + + hover = lsp_server.hover(lsp.HoverParams(text_document=document, position=position)).contents.value + assert hover.startswith("**Block template** `end_of_subprogram` — oem.def:5") + assert "Text[RET]" in hover and "Text[M17]" not in hover + assert hover.endswith("_Overrides ctrl.def:5_") + + [location] = lsp_server.goto_definition(lsp.DefinitionParams(text_document=document, position=position)) + assert location.uri.endswith("oem/oem.def") + references = lsp_server.references( + lsp.ReferenceParams(text_document=document, position=position, context=lsp.ReferenceContext(include_declaration=True)) + ) + assert {Path(location.uri).name for location in references} == {"ctrl.def", "oem.def", "caller.tcl"}