test(server): add tests for .def completions and uplevel formatting
This commit is contained in:
@@ -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 = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Configuration>
|
||||
<Sourcing><Sequence>
|
||||
<Layer Name="Service" SubFolder="service">
|
||||
<Scripts><Filename Name="service"/></Scripts>
|
||||
<DefinedEvents><Filename Name="service" Processing="auto"/></DefinedEvents>
|
||||
</Layer>
|
||||
<Layer Name="Empty"><DefinedEvents/></Layer>
|
||||
</Sequence></Sourcing>
|
||||
</Configuration>
|
||||
"""
|
||||
|
||||
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))
|
||||
@@ -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"
|
||||
)
|
||||
Reference in New Issue
Block a user