feat(lsp): add call hierarchy support for TCL and MOM
Adds LSP call hierarchy support for TCL procedures and MOM events. A symbol index and identity logic underpin incoming and outgoing calls. LSP call hierarchy features are wired and changelog/README updated. - Implement data encoding for call hierarchy items and identity restoration - Wire LSP server to expose prepare_call_hierarchy, incoming_calls, and outgoing_calls - Add tests for cross-file calls and edge cases
This commit is contained in:
@@ -1,22 +1,25 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
THIS_DIR = Path(__file__).parent
|
||||
SRC_DIR = THIS_DIR.parent.parent / "src"
|
||||
if str(SRC_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(SRC_DIR))
|
||||
|
||||
import lsprotocol.types as lsp # type: ignore
|
||||
from pygls.workspace.text_document import TextDocument
|
||||
|
||||
import lsp_server
|
||||
import lsprotocol.types as lsp # type: ignore
|
||||
from lsp_tclserver import TclLanguageServer
|
||||
from lsprotocol.converters import get_converter
|
||||
from pygls.workspace.text_document import TextDocument
|
||||
from tools.navigation import (
|
||||
SymbolIdentity,
|
||||
build_file_symbol_index,
|
||||
call_hierarchy_identity,
|
||||
call_hierarchy_items,
|
||||
definition_identities,
|
||||
incoming_call_hierarchy,
|
||||
matching_occurrences,
|
||||
outgoing_call_hierarchy,
|
||||
symbol_at_position,
|
||||
workspace_symbols,
|
||||
)
|
||||
@@ -269,3 +272,132 @@ def test_duplicate_proc_definition_cannot_be_renamed(tmp_path: Path, monkeypatch
|
||||
)
|
||||
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_call_hierarchy_tracks_cross_file_event_calls(tmp_path: Path, monkeypatch):
|
||||
library_source = """proc leaf {} { return }
|
||||
namespace eval shop {
|
||||
proc middle {} {
|
||||
::leaf
|
||||
}
|
||||
}
|
||||
"""
|
||||
event_source = """proc MOM_linear_move {} {
|
||||
::shop::middle
|
||||
::shop::middle
|
||||
puts done
|
||||
}
|
||||
"""
|
||||
bootstrap_source = "::shop::middle\n"
|
||||
library = _document(tmp_path / "library.tcl", library_source)
|
||||
event = _document(tmp_path / "event.tcl", event_source)
|
||||
bootstrap = _document(tmp_path / "bootstrap.tcl", bootstrap_source)
|
||||
server = TclLanguageServer(
|
||||
name="call-hierarchy-test", version="1", max_workers=1
|
||||
)
|
||||
assert server.update_poco_completion_for_file(library)
|
||||
assert server.update_poco_completion_for_file(event)
|
||||
assert server.update_poco_completion_for_file(bootstrap)
|
||||
monkeypatch.setattr(lsp_server, "LSP_SERVER", server)
|
||||
|
||||
prepared = lsp_server.prepare_call_hierarchy(
|
||||
lsp.CallHierarchyPrepareParams(
|
||||
text_document=lsp.TextDocumentIdentifier(uri=event.uri),
|
||||
position=_position(event_source, "middle"),
|
||||
)
|
||||
)
|
||||
|
||||
assert prepared is not None
|
||||
assert len(prepared) == 1
|
||||
middle = prepared[0]
|
||||
assert middle.name == "shop::middle"
|
||||
assert middle.kind == lsp.SymbolKind.Function
|
||||
assert middle.uri == library.uri
|
||||
assert call_hierarchy_identity(middle) == SymbolIdentity(
|
||||
kind="proc", name="::shop::middle"
|
||||
)
|
||||
|
||||
incoming = lsp_server.incoming_calls(
|
||||
lsp.CallHierarchyIncomingCallsParams(item=middle)
|
||||
)
|
||||
assert len(incoming) == 2
|
||||
event_call = next(
|
||||
call for call in incoming if call.from_.kind == lsp.SymbolKind.Event
|
||||
)
|
||||
file_call = next(
|
||||
call for call in incoming if call.from_.kind == lsp.SymbolKind.File
|
||||
)
|
||||
assert event_call.from_.name == "MOM_linear_move"
|
||||
assert len(event_call.from_ranges) == 2
|
||||
assert file_call.from_.name == "bootstrap.tcl"
|
||||
assert len(file_call.from_ranges) == 1
|
||||
incoming_payload = get_converter().unstructure(event_call)
|
||||
assert incoming_payload["from"]["name"] == "MOM_linear_move"
|
||||
assert len(incoming_payload["fromRanges"]) == 2
|
||||
assert incoming_payload["from"]["data"]["source"] == (
|
||||
"nx-post-support.call-hierarchy"
|
||||
)
|
||||
|
||||
outgoing = lsp_server.outgoing_calls(
|
||||
lsp.CallHierarchyOutgoingCallsParams(item=middle)
|
||||
)
|
||||
assert len(outgoing) == 1
|
||||
assert outgoing[0].to.name == "leaf"
|
||||
assert _range_text(library_source, outgoing[0].from_ranges[0]) == "leaf"
|
||||
|
||||
event_item = event_call.from_
|
||||
event_outgoing = lsp_server.outgoing_calls(
|
||||
lsp.CallHierarchyOutgoingCallsParams(item=event_item)
|
||||
)
|
||||
assert len(event_outgoing) == 1
|
||||
assert event_outgoing[0].to.name == "shop::middle"
|
||||
assert len(event_outgoing[0].from_ranges) == 2
|
||||
|
||||
file_outgoing = lsp_server.outgoing_calls(
|
||||
lsp.CallHierarchyOutgoingCallsParams(item=file_call.from_)
|
||||
)
|
||||
assert len(file_outgoing) == 1
|
||||
assert file_outgoing[0].to.name == "shop::middle"
|
||||
assert len(file_outgoing[0].from_ranges) == 1
|
||||
|
||||
|
||||
def test_call_hierarchy_ignores_dynamic_and_ambiguous_calls(tmp_path: Path):
|
||||
caller_source = """proc caller {command} {
|
||||
$command
|
||||
duplicate
|
||||
}
|
||||
"""
|
||||
caller = _index(tmp_path / "caller.tcl", caller_source)
|
||||
duplicate_a = _index(tmp_path / "duplicate_a.tcl", "proc duplicate {} {}\n")
|
||||
duplicate_b = _index(tmp_path / "duplicate_b.tcl", "proc duplicate {} {}\n")
|
||||
indexes = {
|
||||
caller.path: caller,
|
||||
duplicate_a.path: duplicate_a,
|
||||
duplicate_b.path: duplicate_b,
|
||||
}
|
||||
definitions = definition_identities(indexes)
|
||||
caller_identity = SymbolIdentity(kind="proc", name="::caller")
|
||||
duplicate_identity = SymbolIdentity(kind="proc", name="::duplicate")
|
||||
|
||||
assert call_hierarchy_items(duplicate_identity, indexes) == []
|
||||
assert (
|
||||
incoming_call_hierarchy(duplicate_identity, indexes, definitions) == []
|
||||
)
|
||||
assert outgoing_call_hierarchy(caller_identity, indexes, definitions) == []
|
||||
|
||||
|
||||
def test_call_hierarchy_item_uses_whole_proc_range(tmp_path: Path):
|
||||
source = """proc multiline {} {
|
||||
return
|
||||
}
|
||||
"""
|
||||
index = _index(tmp_path / "range.tcl", source)
|
||||
items = call_hierarchy_items(
|
||||
SymbolIdentity(kind="proc", name="::multiline"),
|
||||
{index.path: index},
|
||||
)
|
||||
|
||||
assert len(items) == 1
|
||||
assert items[0].selection_range.start == lsp.Position(line=0, character=5)
|
||||
assert items[0].range.start == lsp.Position(line=0, character=0)
|
||||
assert items[0].range.end.line == 2
|
||||
|
||||
Reference in New Issue
Block a user