diff --git a/server/tests/python_tests/test_def_symbols.py b/server/tests/python_tests/test_def_symbols.py new file mode 100644 index 0000000..b7afc86 --- /dev/null +++ b/server/tests/python_tests/test_def_symbols.py @@ -0,0 +1,200 @@ +"""Completion of .def block templates and addresses from PSC DefinedEvents.""" + +import itertools +from pathlib import Path + +import lsprotocol.types as lsp # type: ignore +from pygls.workspace import Workspace + +import lsp_server +from lsp_tclserver import TclLanguageServer +from tools.def_symbols import parse_def_symbols +from tools.file_sourcing import psc_defined_event_files + +PSC = """ + + + + + + + + + +""" + +DEF = """MACHINE Default + +FORMATTING +{ + ADDRESS SPOS + { + FORMAT Coordinate + } + ADDRESS X { + } +\tBLOCK_TEMPLATE external_subprogram +\t{ +\t\tText[$lib_spf(value,subprogram_output_name)] +\t} +\tBLOCK_TEMPLATE steady_rest +\t{ +\t\tText[M60] +\t} +#\tBLOCK_TEMPLATE commented_out +} +""" + + +def _project(tmp_path: Path, monkeypatch): + (tmp_path / "service").mkdir() + psc = tmp_path / "post.psc" + psc.write_text(PSC, encoding="utf-8") + (tmp_path / "service" / "service.def").write_text(DEF, encoding="utf-8") + (tmp_path / "service" / "service.tcl").write_text("proc svc {} {}\n", encoding="utf-8") + + server = TclLanguageServer(name="block-template-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]) + return server, psc + + +_versions = itertools.count(1) + + +def _complete(server, tmp_path: Path, source: str, trigger: str | None = None): + uri = (tmp_path / "caller.tcl").as_uri() + # A new version per request, the server caches lines by (uri, version). + server.workspace.put_text_document( + lsp.TextDocumentItem(uri=uri, language_id="tcl", version=next(_versions), text=source) + ) + lines = source.split("\n") + context = None + if trigger is not None: + context = lsp.CompletionContext( + trigger_kind=lsp.CompletionTriggerKind.TriggerCharacter, + trigger_character=trigger, + ) + result = lsp_server.on_completion( + lsp.CompletionParams( + text_document=lsp.TextDocumentIdentifier(uri=uri), + position=lsp.Position(line=len(lines) - 1, character=len(lines[-1])), + context=context, + ) + ) + return sorted(result.items, key=lambda item: item.sort_text or "") + + +def test_parse_def_symbols_ignores_comments(): + symbols = parse_def_symbols(DEF) + assert symbols.block_templates == ("external_subprogram", "steady_rest") + assert symbols.addresses == ("SPOS", "X") + + +def test_psc_defined_event_files_resolve_def_paths(tmp_path, monkeypatch): + _, psc = _project(tmp_path, monkeypatch) + assert psc_defined_event_files(psc) == [(tmp_path / "service" / "service.def").resolve()] + + +def test_mom_do_template_offers_templates_then_variables_on_space(tmp_path, monkeypatch): + server, _ = _project(tmp_path, monkeypatch) + items = _complete(server, tmp_path, "set globalValue 1\nMOM_do_template ", trigger=" ") + labels = [item.label for item in items] + assert labels[:2] == ["external_subprogram", "steady_rest"] + assert items[0].detail == "Block template (service.def)" + variable = next(item for item in items if item.label == "globalValue") + assert variable.insert_text == "$globalValue" + assert "svc" not in labels + + +def test_mom_do_template_ranks_templates_before_variables(tmp_path, monkeypatch): + server, _ = _project(tmp_path, monkeypatch) + items = _complete(server, tmp_path, "set globalValue 1\nMOM_do_template st") + labels = [item.label for item in items] + assert labels[:2] == ["external_subprogram", "steady_rest"] + assert "globalValue" in labels + + +def test_mom_do_template_dollar_still_completes_variables(tmp_path, monkeypatch): + server, _ = _project(tmp_path, monkeypatch) + items = _complete(server, tmp_path, "set globalValue 1\nMOM_do_template $glob") + labels = {item.label for item in items} + assert "globalValue" in labels + assert "steady_rest" not in labels + + +def test_def_change_refreshes_block_templates(tmp_path, monkeypatch): + server, _ = _project(tmp_path, monkeypatch) + def_file = tmp_path / "service" / "service.def" + def_file.write_text("BLOCK_TEMPLATE new_block\n{\n}\n", encoding="utf-8") + lsp_server.did_change_watched_files(lsp.DidChangeWatchedFilesParams(changes=[ + lsp.FileEvent(uri=def_file.as_uri(), type=lsp.FileChangeType.Changed)])) + assert [item.label for item in server.block_template_items()] == ["new_block"] + assert server.address_items() == [] + + +def test_mom_ask_address_value_offers_addresses_then_variables(tmp_path, monkeypatch): + server, _ = _project(tmp_path, monkeypatch) + items = _complete(server, tmp_path, "set globalValue 1\nMOM_ask_address_value ", trigger=" ") + labels = [item.label for item in items] + assert labels[:2] == ["SPOS", "X"] + assert items[0].detail == "Address (service.def)" + assert "globalValue" in labels + assert "steady_rest" not in labels + + +def test_mom_force_and_suppress_offer_mode_or_variable_then_addresses(tmp_path, monkeypatch): + server, _ = _project(tmp_path, monkeypatch) + for command in ("MOM_force", "MOM_suppress"): + items = _complete(server, tmp_path, f"set globalValue 1\n{command} ", trigger=" ") + labels = [item.label for item in items] + assert set(labels[:3]) == {"Always", "Once", "Off"} + variable = next(item for item in items if item.label == "globalValue") + assert variable.insert_text == "$globalValue" + assert "SPOS" not in labels + + for source in (f"{command} Once ", f"{command} Always SPOS "): + labels = [item.label for item in _complete(server, tmp_path, source, trigger=" ")] + assert labels[:2] == ["SPOS", "X"] + assert "Once" not in labels + + +def _edit(items, label): + item = next(item for item in items if item.label == label) + return item.text_edit.new_text, (item.text_edit.range.start.character, item.text_edit.range.end.character) + + +def test_def_symbols_and_modes_are_inserted_quoted(tmp_path, monkeypatch): + server, _ = _project(tmp_path, monkeypatch) + source = "MOM_do_template st" + assert _edit(_complete(server, tmp_path, source), "steady_rest") == ('"steady_rest"', (16, 18)) + source = "MOM_force " + assert _edit(_complete(server, tmp_path, source, trigger=" "), "Once") == ('"Once"', (10, 10)) + source = "MOM_force Once SP" + assert _edit(_complete(server, tmp_path, source), "SPOS") == ('"SPOS"', (15, 17)) + + +def test_typed_quotes_are_replaced_not_doubled(tmp_path, monkeypatch): + server, _ = _project(tmp_path, monkeypatch) + # Opening quote typed by the user. + source = 'MOM_ask_address_value "SP' + assert _edit(_complete(server, tmp_path, source), "SPOS") == ('"SPOS"', (22, 25)) + # Closing quote inserted by the editor after the cursor. + uri = (tmp_path / "caller.tcl").as_uri() + source = 'MOM_ask_address_value "SP"' + server.workspace.put_text_document( + lsp.TextDocumentItem(uri=uri, language_id="tcl", version=next(_versions), text=source) + ) + items = lsp_server.on_completion( + lsp.CompletionParams( + text_document=lsp.TextDocumentIdentifier(uri=uri), + position=lsp.Position(line=0, character=25), + ) + ).items + assert _edit(items, "SPOS") == ('"SPOS"', (22, 26)) diff --git a/server/tests/python_tests/test_format_uplevel.py b/server/tests/python_tests/test_format_uplevel.py new file mode 100644 index 0000000..3532b7c --- /dev/null +++ b/server/tests/python_tests/test_format_uplevel.py @@ -0,0 +1,33 @@ +"""Formatting of uplevel bodies.""" + +from tclint.format import FormatterOpts +from tools.formatter import NxFormatter +from tools.parser import CustomParser + + +def _format(source: str) -> str: + formatter = NxFormatter( + FormatterOpts( + indent="\t", + spaces_in_braces=False, + balanced_spaces_in_braces=False, + max_blank_lines=500, + indent_namespace_eval=True, + indent_mixed_tab_size=0, + emacs=False, + debug_whitespace=False, + ) + ) + return formatter.format_top(source, CustomParser()) + + +def test_uplevel_body_is_indented(): + source = "proc a {} {\n\tuplevel #0 {\n\tset x 1\n\t\tset y 2\n }\n}\n" + assert _format(source) == "proc a {} {\n\tuplevel #0 {\n\t\tset x 1\n\t\tset y 2\n\t}\n}\n" + + +def test_uplevel_without_level_and_with_variable_level(): + source = "uplevel {\nset x 1\n}\nuplevel $lvl {\nset y 2\n}\nuplevel set z 3\n" + assert _format(source) == ( + "uplevel {\n\tset x 1\n}\nuplevel $lvl {\n\tset y 2\n}\nuplevel set z 3\n" + )