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 from lsp_tclserver import TclLanguageServer from tools.navigation import ( SymbolIdentity, build_file_symbol_index, definition_identities, matching_occurrences, symbol_at_position, workspace_symbols, ) from tools.parser import CustomParser def _index(path: Path, source: str): return build_file_symbol_index( str(path), path.as_uri(), CustomParser().parse(source) ) def _document(path: Path, source: str) -> TextDocument: return TextDocument( uri=path.as_uri(), source=source, version=1, language_id="tcl", ) def _position(source: str, token: str, occurrence: int = 0) -> lsp.Position: offset = -1 for _ in range(occurrence + 1): offset = source.index(token, offset + 1) before = source[:offset] return lsp.Position( line=before.count("\n"), character=offset - (before.rfind("\n") + 1), ) def _range_text(source: str, range_: lsp.Range) -> str: assert range_.start.line == range_.end.line line = source.splitlines()[range_.start.line] return line[range_.start.character : range_.end.character] def test_proc_references_respect_namespaces_and_root_fallback(tmp_path: Path): first_source = """proc shared {value} { return $value } namespace eval shop { proc shared {value} { return $value } proc call {} { shared 1 } } """ second_source = """shared 2 namespace eval shop { shared 3 } ::shop::shared 4 """ first = _index(tmp_path / "first.tcl", first_source) second = _index(tmp_path / "second.tcl", second_source) indexes = {first.path: first, second.path: second} definitions = definition_identities(indexes) root = SymbolIdentity(kind="proc", name="::shared") namespaced = SymbolIdentity(kind="proc", name="::shop::shared") assert len(matching_occurrences(root, indexes, definitions)) == 2 assert len(matching_occurrences(namespaced, indexes, definitions)) == 4 def test_local_variable_identity_does_not_leak_between_procs(tmp_path: Path): source = """proc first {} { set value 1 puts $value } proc second {} { set value 2 puts $value } """ index = _index(tmp_path / "locals.tcl", source) indexes = {index.path: index} definitions = definition_identities(indexes) position = _position(source, "$value") result = symbol_at_position( index, lsp.Position(position.line, position.character + 1), definitions, ) assert result is not None _, identity = result matches = matching_occurrences(identity, indexes, definitions) assert len(matches) == 2 assert all(match.identity.scope and "::first" in match.identity.scope for _, match in matches) def test_foreach_binding_can_be_renamed_without_touching_other_proc( tmp_path: Path, ): source = """proc first {items} { foreach item $items { puts $item } } proc second {items} { foreach item $items { puts $item } } """ index = _index(tmp_path / "foreach.tcl", source) indexes = {index.path: index} definitions = definition_identities(indexes) position = _position(source, "item", occurrence=1) result = symbol_at_position(index, position, definitions) assert result is not None _, identity = result matches = matching_occurrences(identity, indexes, definitions) assert identity in definitions assert len(matches) == 2 assert all("::first" in (occurrence.identity.scope or "") for _, occurrence in matches) def test_variable_ranges_preserve_qualifiers_and_tcl_substitution(tmp_path: Path): source = """namespace eval shop { variable value 0 proc use {} { variable value set value 1 puts ${value} } } set ::shop::value 2 """ index = _index(tmp_path / "variables.tcl", source) indexes = {index.path: index} definitions = definition_identities(indexes) identity = SymbolIdentity(kind="variable", name="::shop::value") matches = matching_occurrences(identity, indexes, definitions) assert len(matches) == 5 assert all(_range_text(source, occurrence.range) == "value" for _, occurrence in matches) def test_qualified_proc_body_and_variable_import_use_declared_namespace( tmp_path: Path, ): source = """namespace eval current { proc ::other::use {} { variable value puts $value variable ::external::setting puts $setting } } namespace eval other { variable value 1 } namespace eval external { variable setting 2 } """ index = _index(tmp_path / "qualified.tcl", source) indexes = {index.path: index} definitions = definition_identities(indexes) other_value = SymbolIdentity(kind="variable", name="::other::value") external_setting = SymbolIdentity( kind="variable", name="::external::setting" ) assert len(matching_occurrences(other_value, indexes, definitions)) == 3 assert len(matching_occurrences(external_setting, indexes, definitions)) == 3 def test_workspace_symbols_include_procs_namespaces_and_global_variables(tmp_path: Path): source = """set globalValue 1 set globalValue 2 proc rootProc {} { return } namespace eval shop { proc namespacedProc {} { return } } """ index = _index(tmp_path / "symbols.tcl", source) symbols = workspace_symbols({index.path: index}, "") names = [symbol.name for symbol in symbols] assert names.count("globalValue") == 1 assert "rootProc" in names assert "shop" in names assert "shop::namespacedProc" in names def test_lsp_references_definition_rename_and_workspace_symbols( tmp_path: Path, monkeypatch ): declaration_source = "proc customProc {value} { return $value }\n" usage_source = "set result [customProc 1]\n" declaration = _document(tmp_path / "declaration.tcl", declaration_source) usage = _document(tmp_path / "usage.tcl", usage_source) server = TclLanguageServer(name="navigation-test", version="1", max_workers=1) assert server.update_poco_completion_for_file(declaration) assert server.update_poco_completion_for_file(usage) monkeypatch.setattr(lsp_server, "LSP_SERVER", server) position = _position(usage_source, "customProc") identifier = lsp.TextDocumentIdentifier(uri=usage.uri) definitions = lsp_server.goto_definition( lsp.DefinitionParams(text_document=identifier, position=position) ) assert definitions is not None assert len(definitions) == 1 assert definitions[0].uri == declaration.uri references = lsp_server.references( lsp.ReferenceParams( text_document=identifier, position=position, context=lsp.ReferenceContext(include_declaration=True), ) ) assert len(references) == 2 prepared = lsp_server.prepare_rename( lsp.PrepareRenameParams(text_document=identifier, position=position) ) assert prepared is not None assert prepared.placeholder == "customProc" edit = lsp_server.rename( lsp.RenameParams( text_document=identifier, position=position, new_name="renamedProc", ) ) assert edit is not None assert edit.changes is not None assert set(edit.changes) == {declaration.uri, usage.uri} assert all( text_edit.new_text == "renamedProc" for edits in edit.changes.values() for text_edit in edits ) symbols = lsp_server.workspace_symbol( lsp.WorkspaceSymbolParams(query="custom") ) assert [symbol.name for symbol in symbols] == ["customProc"] def test_duplicate_proc_definition_cannot_be_renamed(tmp_path: Path, monkeypatch): server = TclLanguageServer(name="navigation-test", version="1", max_workers=1) documents = [ _document(tmp_path / f"duplicate_{number}.tcl", "proc duplicate {} { return }") for number in range(2) ] for document in documents: assert server.update_poco_completion_for_file(document) monkeypatch.setattr(lsp_server, "LSP_SERVER", server) result = lsp_server.prepare_rename( lsp.PrepareRenameParams( text_document=lsp.TextDocumentIdentifier(uri=documents[0].uri), position=lsp.Position(line=0, character=6), ) ) assert result is None