Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a857dab85a | ||
|
|
28bd6557c3 | ||
|
|
0a8e8a7368 | ||
|
|
bd9c73452e | ||
|
|
a393ca3aec | ||
|
|
85a8684254 |
+1
-1
@@ -2,7 +2,7 @@
|
||||
"name": "nx-post-support",
|
||||
"displayName": "NX Postprocessor Support",
|
||||
"description": "VS Code extension for NX CAM postprocessor development with language support and remote Tcl debugging for CDL, TCL, and DEF files",
|
||||
"version": "2026.9.701",
|
||||
"version": "2026.9.800",
|
||||
"publisher": "Christoph",
|
||||
"icon": "images/nx-1.png",
|
||||
"activationEvents": [
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
"""Navigation between Tcl code and the block templates and addresses of .def files."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import lsprotocol.types as lsp
|
||||
|
||||
from tools.def_symbols import ADDRESS, BLOCK_TEMPLATE, FORMAT, DefDeclaration, DefDocument
|
||||
from tools.navigation import FileSymbolIndex, SymbolOccurrence
|
||||
|
||||
DefTarget = tuple[str, str]
|
||||
_KIND_LABELS = {BLOCK_TEMPLATE: "Block template", ADDRESS: "Address", FORMAT: "Format"}
|
||||
# Address properties in display order; others follow as declared.
|
||||
_ADDRESS_PROPERTIES = (
|
||||
("FORMAT", "Format"),
|
||||
("LEADER", "Leader"),
|
||||
("TRAILER", "Trailer"),
|
||||
("MIN", "Min"),
|
||||
("MAX", "Max"),
|
||||
("FORCE", "Modality"),
|
||||
("ZERO_FORMAT", "Zero format"),
|
||||
("INCREMENTAL", "Incremental"),
|
||||
("OMIT", "Omit"),
|
||||
)
|
||||
_MODALITY = {
|
||||
"OFF": "modal, output only on change",
|
||||
"ONCE": "output once, then modal",
|
||||
"ALWAYS": "non-modal, always output",
|
||||
}
|
||||
|
||||
|
||||
def _range(line: int, start: int, end: int) -> lsp.Range:
|
||||
return lsp.Range(
|
||||
start=lsp.Position(line=line, character=start),
|
||||
end=lsp.Position(line=line, character=end),
|
||||
)
|
||||
|
||||
|
||||
def _contains(line: int, start: int, end: int, position: lsp.Position) -> bool:
|
||||
return position.line == line and start <= position.character <= end
|
||||
|
||||
|
||||
def def_symbol_at(document: DefDocument, position: lsp.Position) -> tuple[DefTarget, lsp.Range, bool] | None:
|
||||
"""Return the block template or address at ``position`` of a .def document.
|
||||
|
||||
The flag tells whether the position is on a declaration.
|
||||
"""
|
||||
for declaration in document.declarations:
|
||||
if declaration.kind != FORMAT and _contains(declaration.line, declaration.start, declaration.end, position):
|
||||
return (declaration.kind, declaration.name), _range(declaration.line, declaration.start, declaration.end), True
|
||||
for reference in document.references:
|
||||
if _contains(reference.line, reference.start, reference.end, position):
|
||||
return (reference.kind, reference.name), _range(reference.line, reference.start, reference.end), False
|
||||
return None
|
||||
|
||||
|
||||
def _uri(path: str, uris: dict[str, str]) -> str:
|
||||
return uris.get(path) or Path(path).as_uri()
|
||||
|
||||
|
||||
def def_declarations(documents: dict[str, DefDocument], target: DefTarget) -> list[tuple[str, DefDeclaration]]:
|
||||
kind, name = target
|
||||
return [
|
||||
(path, declaration)
|
||||
for path, document in documents.items()
|
||||
for declaration in document.declarations
|
||||
if declaration.kind == kind and declaration.name == name
|
||||
]
|
||||
|
||||
|
||||
def def_definition_locations(
|
||||
documents: dict[str, DefDocument], target: DefTarget, uris: dict[str, str] | None = None
|
||||
) -> list[lsp.Location]:
|
||||
uris = uris or {}
|
||||
return [
|
||||
lsp.Location(uri=_uri(path, uris), range=_range(declaration.line, declaration.start, declaration.end))
|
||||
for path, declaration in def_declarations(documents, target)
|
||||
]
|
||||
|
||||
|
||||
def def_reference_locations(
|
||||
documents: dict[str, DefDocument],
|
||||
target: DefTarget,
|
||||
include_declaration: bool,
|
||||
uris: dict[str, str] | None = None,
|
||||
) -> list[lsp.Location]:
|
||||
"""Return .def occurrences: declarations and addresses used in block templates."""
|
||||
kind, name = target
|
||||
uris = uris or {}
|
||||
locations = def_definition_locations(documents, target, uris) if include_declaration else []
|
||||
for path, document in documents.items():
|
||||
for reference in document.references:
|
||||
if reference.kind == kind and reference.name == name:
|
||||
locations.append(
|
||||
lsp.Location(uri=_uri(path, uris), range=_range(reference.line, reference.start, reference.end))
|
||||
)
|
||||
return locations
|
||||
|
||||
|
||||
def tcl_def_occurrences(
|
||||
indexes: dict[str, FileSymbolIndex], target: DefTarget
|
||||
) -> list[tuple[FileSymbolIndex, SymbolOccurrence]]:
|
||||
kind, name = target
|
||||
return [
|
||||
(index, occurrence)
|
||||
for index in indexes.values()
|
||||
for occurrence in index.occurrences
|
||||
if occurrence.identity.kind == kind and occurrence.identity.name == name
|
||||
]
|
||||
|
||||
|
||||
def all_def_target_locations(
|
||||
documents: dict[str, DefDocument],
|
||||
indexes: dict[str, FileSymbolIndex],
|
||||
target: DefTarget,
|
||||
include_declaration: bool,
|
||||
uris: dict[str, str] | None = None,
|
||||
) -> list[lsp.Location]:
|
||||
locations = def_reference_locations(documents, target, include_declaration, uris)
|
||||
locations.extend(
|
||||
lsp.Location(uri=index.uri, range=occurrence.range) for index, occurrence in tcl_def_occurrences(indexes, target)
|
||||
)
|
||||
unique = {}
|
||||
for location in locations:
|
||||
key = (location.uri, location.range.start.line, location.range.start.character)
|
||||
unique.setdefault(key, location)
|
||||
return sorted(unique.values(), key=lambda item: (item.uri, item.range.start.line, item.range.start.character))
|
||||
|
||||
|
||||
def def_rename_edits(
|
||||
documents: dict[str, DefDocument],
|
||||
indexes: dict[str, FileSymbolIndex],
|
||||
target: DefTarget,
|
||||
new_name: str,
|
||||
uris: dict[str, str] | None = None,
|
||||
) -> lsp.WorkspaceEdit | None:
|
||||
"""Rename a block template or address in all .def and Tcl files, if it is declared."""
|
||||
if not def_declarations(documents, target):
|
||||
return None
|
||||
changes: dict[str, list[lsp.TextEdit]] = {}
|
||||
for location in all_def_target_locations(documents, indexes, target, True, uris):
|
||||
changes.setdefault(location.uri, []).append(lsp.TextEdit(range=location.range, new_text=new_name))
|
||||
for edits in changes.values():
|
||||
edits.sort(key=lambda edit: (edit.range.start.line, edit.range.start.character), reverse=True)
|
||||
return lsp.WorkspaceEdit(changes=changes)
|
||||
|
||||
|
||||
def _escape_cell(value: str) -> str:
|
||||
return value.replace("|", "\\|") or " "
|
||||
|
||||
|
||||
def _address_table(declaration: DefDeclaration, formats: dict[str, DefDeclaration]) -> str:
|
||||
properties = dict(declaration.properties)
|
||||
rows = []
|
||||
for key, label in _ADDRESS_PROPERTIES:
|
||||
value = properties.pop(key, None)
|
||||
if value is None:
|
||||
continue
|
||||
cell = f"`{value}`" if value else ""
|
||||
if key == "FORMAT" and value in formats:
|
||||
cell += f" → `{dict(formats[value].properties).get('FORMAT', '')}`"
|
||||
if key == "FORCE":
|
||||
cell += f" ({_MODALITY[value.upper()]})" if value.upper() in _MODALITY else ""
|
||||
rows.append(f"| {label} | {_escape_cell(cell)} |")
|
||||
rows.extend(f"| {key.title()} | {_escape_cell(f'`{value}`' if value else '')} |" for key, value in properties.items())
|
||||
if not rows:
|
||||
return "_No properties_"
|
||||
return "\n".join(["| Property | Value |", "|---|---|", *rows])
|
||||
|
||||
|
||||
def def_hover_markdown(documents: dict[str, DefDocument], target: DefTarget) -> str | None:
|
||||
declarations = def_declarations(documents, target)
|
||||
if not declarations:
|
||||
return None
|
||||
formats = {
|
||||
declaration.name: declaration
|
||||
for document in documents.values()
|
||||
for declaration in document.declarations
|
||||
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)
|
||||
@@ -47,20 +47,9 @@ def _document(path: Path, source: str) -> TextDocument:
|
||||
)
|
||||
|
||||
|
||||
def _completion_server(
|
||||
tmp_path: Path, monkeypatch
|
||||
) -> tuple[TclLanguageServer, TextDocument, str]:
|
||||
def _completion_server(tmp_path: Path, monkeypatch) -> tuple[TclLanguageServer, TextDocument, str]:
|
||||
declared_builtin = standard_items.nx_variables[0].label
|
||||
current_source = (
|
||||
"set globalValue 1\n"
|
||||
"proc localProc {} { return }\n"
|
||||
"proc caller {argument} {\n"
|
||||
f" global {declared_builtin}\n"
|
||||
" set localValue 2\n"
|
||||
" puts $local\n"
|
||||
" localP\n"
|
||||
"}\n"
|
||||
)
|
||||
current_source = f"set globalValue 1\nproc localProc {{}} {{ return }}\nproc caller {{argument}} {{\n global {declared_builtin}\n set localValue 2\n puts $local\n localP\n}}\n"
|
||||
workspace_source = """set ::workspaceValue 1
|
||||
proc workspaceProc {} { return }
|
||||
"""
|
||||
@@ -113,14 +102,10 @@ def test_unset_space_shows_options_then_variables(tmp_path, monkeypatch):
|
||||
items = _complete(document, lsp.Position(line=1, character=len(tail)))
|
||||
labels = {item.label for item in items}
|
||||
if tail in {"unset ", "unset -"}:
|
||||
assert labels == {"-nocomplain", "--"}
|
||||
assert labels == {"nocomplain"}
|
||||
else:
|
||||
assert "globalValue" in labels
|
||||
assert "-nocomplain" not in labels
|
||||
if tail == "unset -nocomplain ":
|
||||
assert "--" in labels
|
||||
else:
|
||||
assert "--" not in labels
|
||||
|
||||
|
||||
def test_array_keys_complete_in_set_and_substitution(tmp_path: Path, monkeypatch):
|
||||
@@ -130,19 +115,16 @@ def test_array_keys_complete_in_set_and_substitution(tmp_path: Path, monkeypatch
|
||||
"set ::lib_flag(enabled) 1\nset ::lib_flag(external) 1\n",
|
||||
)
|
||||
assert server.update_poco_completion_for_file(workspace)
|
||||
source = (
|
||||
"set lib_flag(enabled) 0\n"
|
||||
"set lib_flag(empty) 1\n"
|
||||
"set other(wrong) 1\n"
|
||||
"proc hidden {} { set lib_flag(private) 1 }\n"
|
||||
"set lib_flag()\n"
|
||||
"puts $lib_flag(en)\n"
|
||||
"puts 😀; set lib_flag(em\n"
|
||||
)
|
||||
source = "set lib_flag(enabled) 0\nset lib_flag(empty) 1\nset other(wrong) 1\nproc hidden {} { set lib_flag(private) 1 }\nset lib_flag()\nputs $lib_flag(en)\nputs 😀; set lib_flag(em\n"
|
||||
current = _document(tmp_path / "arrays-current.tcl", source)
|
||||
server.workspace.put_text_document(lsp.TextDocumentItem(
|
||||
uri=current.uri, language_id="tcl", version=1, text=source,
|
||||
))
|
||||
server.workspace.put_text_document(
|
||||
lsp.TextDocumentItem(
|
||||
uri=current.uri,
|
||||
language_id="tcl",
|
||||
version=1,
|
||||
text=source,
|
||||
)
|
||||
)
|
||||
assert server.update_poco_completion_for_file(current)
|
||||
items = _complete(current, _position_after(source, "set lib_flag(", 3))
|
||||
assert [item.label for item in items] == ["empty", "enabled", "external"]
|
||||
@@ -179,32 +161,28 @@ def test_dynamic_array_index_keeps_variable_identity(tmp_path: Path, monkeypatch
|
||||
assert variable_name(tree.children[3].args[0]) is None
|
||||
path = tmp_path / "dynamic.tcl"
|
||||
index = build_file_symbol_index(str(path), path.as_uri(), tree)
|
||||
definition = next(
|
||||
item for item in index.occurrences
|
||||
if item.identity.name == "::custom_flag" and item.is_definition
|
||||
)
|
||||
definition = next(item for item in index.occurrences if item.identity.name == "::custom_flag" and item.is_definition)
|
||||
assert definition.range.start.character == 4
|
||||
assert definition.range.end.character == 15
|
||||
assert definition.array_element is None
|
||||
assert any(item.identity.name == "::mom_path_name" for item in index.occurrences)
|
||||
highlighter = _Highlighter([], {})
|
||||
tree.accept(highlighter, recurse=True)
|
||||
assert any(
|
||||
position == (0, 4) and length == 11 and kind == "variable"
|
||||
for position, length, kind, _ in highlighter._tokens
|
||||
)
|
||||
assert any(position == (0, 4) and length == 11 and kind == "variable" for position, length, kind, _ in highlighter._tokens)
|
||||
server, _, _ = _completion_server(tmp_path, monkeypatch)
|
||||
current = _document(path, source)
|
||||
server.workspace.put_text_document(lsp.TextDocumentItem(
|
||||
uri=current.uri, language_id="tcl", version=1, text=source,
|
||||
))
|
||||
server.workspace.put_text_document(
|
||||
lsp.TextDocumentItem(
|
||||
uri=current.uri,
|
||||
language_id="tcl",
|
||||
version=1,
|
||||
text=source,
|
||||
)
|
||||
)
|
||||
assert server.update_poco_completion_for_file(current)
|
||||
items = _complete(current, _position_after(source, "puts $custom"))
|
||||
assert "custom_flag" in {item.label for item in items}
|
||||
workspace_items = next(
|
||||
items for item_path, items in server.completion_items_by_file_snapshot().items()
|
||||
if server.paths_equal(item_path, str(path))
|
||||
)
|
||||
workspace_items = next(items for item_path, items in server.completion_items_by_file_snapshot().items() if server.paths_equal(item_path, str(path)))
|
||||
assert {"quoted_flag", "command_flag"} <= {item.label for item in workspace_items}
|
||||
|
||||
|
||||
@@ -236,18 +214,25 @@ def test_literal_array_components_complete_around_substitutions(tmp_path: Path,
|
||||
offset = marked.index("|")
|
||||
line = marked.replace("|", "")
|
||||
items = array_element_completions(
|
||||
[line], lsp.Position(line=0, character=offset),
|
||||
server.navigation_snapshot().values(), str(tmp_path / "caller.tcl"),
|
||||
[line],
|
||||
lsp.Position(line=0, character=offset),
|
||||
server.navigation_snapshot().values(),
|
||||
str(tmp_path / "caller.tcl"),
|
||||
)
|
||||
assert {item.label for item in items} == labels
|
||||
edit = next(item.text_edit for item in items if item.label == selected)
|
||||
item = next(item for item in items if item.label == selected)
|
||||
assert item.insert_text_format == lsp.InsertTextFormat.PlainText
|
||||
assert line[:edit.range.start.character] + edit.new_text + line[edit.range.end.character:] == expected
|
||||
assert array_element_completions(
|
||||
["set custom_flag(from_move,$::mom"], lsp.Position(line=0, character=31),
|
||||
server.navigation_snapshot().values(), str(tmp_path / "caller.tcl"),
|
||||
) is None
|
||||
assert line[: edit.range.start.character] + edit.new_text + line[edit.range.end.character :] == expected
|
||||
assert (
|
||||
array_element_completions(
|
||||
["set custom_flag(from_move,$::mom"],
|
||||
lsp.Position(line=0, character=31),
|
||||
server.navigation_snapshot().values(),
|
||||
str(tmp_path / "caller.tcl"),
|
||||
)
|
||||
is None
|
||||
)
|
||||
|
||||
|
||||
def _argument_completion_request(source: str):
|
||||
@@ -277,9 +262,7 @@ def test_variable_completion_filters_and_ranks_candidates(tmp_path: Path, monkey
|
||||
other_builtin = standard_items.nx_variables[1]
|
||||
assert declared_builtin.label in by_label
|
||||
assert other_builtin.label in by_label
|
||||
assert by_label[declared_builtin.label].documentation == (
|
||||
declared_builtin.documentation
|
||||
)
|
||||
assert by_label[declared_builtin.label].documentation == (declared_builtin.documentation)
|
||||
assert by_label["localValue"].sort_text.startswith("000:")
|
||||
assert by_label["globalValue"].sort_text.startswith("100:")
|
||||
assert by_label["workspaceValue"].sort_text.startswith("200:")
|
||||
@@ -307,18 +290,9 @@ def test_command_completion_filters_and_ranks_candidates(tmp_path: Path, monkeyp
|
||||
|
||||
|
||||
def test_completion_context_handles_nested_commands_and_utf16():
|
||||
assert (
|
||||
completion_context(["set result [work"], lsp.Position(line=0, character=16))
|
||||
== CompletionContext.COMMAND
|
||||
)
|
||||
assert (
|
||||
completion_context(["😀 puts $value"], lsp.Position(line=0, character=14))
|
||||
== CompletionContext.VARIABLE
|
||||
)
|
||||
assert (
|
||||
completion_context(["puts value"], lsp.Position(line=0, character=10))
|
||||
== CompletionContext.GENERAL
|
||||
)
|
||||
assert completion_context(["set result [work"], lsp.Position(line=0, character=16)) == CompletionContext.COMMAND
|
||||
assert completion_context(["😀 puts $value"], lsp.Position(line=0, character=14)) == CompletionContext.VARIABLE
|
||||
assert completion_context(["puts value"], lsp.Position(line=0, character=10)) == CompletionContext.GENERAL
|
||||
|
||||
|
||||
def test_string_subcommands_and_compare_options_are_context_aware():
|
||||
@@ -330,9 +304,7 @@ def test_string_subcommands_and_compare_options_are_context_aware():
|
||||
"-length",
|
||||
"-nocase",
|
||||
}
|
||||
assert _argument_completion_labels("string compare -nocase ") == {
|
||||
"-length"
|
||||
}
|
||||
assert _argument_completion_labels("string compare -nocase ") == {"-length"}
|
||||
assert _argument_completion_labels("string compare -length ") is None
|
||||
|
||||
|
||||
@@ -356,22 +328,14 @@ def test_string_completion_inside_braced_conditions_and_bodies():
|
||||
subcommands = _argument_completion_labels(prefix + "[string ")
|
||||
assert subcommands is not None
|
||||
assert {"compare", "equal", "is"} <= subcommands
|
||||
assert _argument_completion_labels(prefix + "[string compare -") == {
|
||||
"-length", "-nocase"
|
||||
}
|
||||
assert _argument_completion_labels(
|
||||
prefix + "[string compare -nocase "
|
||||
) == {"-length"}
|
||||
assert _argument_completion_labels(prefix + "[string compare -") == {"-length", "-nocase"}
|
||||
assert _argument_completion_labels(prefix + "[string compare -nocase ") == {"-length"}
|
||||
|
||||
|
||||
def test_closed_braced_arguments_do_not_change_completion_context():
|
||||
assert _argument_completion_labels("puts {[string compare }") is None
|
||||
assert _argument_completion_labels(
|
||||
"if {[string equal a b]} {string compare "
|
||||
) == {"-length", "-nocase"}
|
||||
assert _argument_completion_labels(
|
||||
"if {[string equal a b] && [string is integer "
|
||||
) == {"-failindex", "-strict"}
|
||||
assert _argument_completion_labels("if {[string equal a b]} {string compare ") == {"-length", "-nocase"}
|
||||
assert _argument_completion_labels("if {[string equal a b] && [string is integer ") == {"-failindex", "-strict"}
|
||||
|
||||
|
||||
def test_dict_array_namespace_file_and_info_subcommands():
|
||||
@@ -413,9 +377,7 @@ def test_dict_array_namespace_file_and_info_subcommands():
|
||||
assert {"args", "body", "commands", "exists", "procs", "vars"} <= info_items
|
||||
|
||||
|
||||
def test_variable_context_still_takes_priority_inside_tcl_command(
|
||||
tmp_path: Path, monkeypatch
|
||||
):
|
||||
def test_variable_context_still_takes_priority_inside_tcl_command(tmp_path: Path, monkeypatch):
|
||||
_, current, source = _completion_server(tmp_path, monkeypatch)
|
||||
command_source = source.replace(
|
||||
" puts $local\n",
|
||||
@@ -437,9 +399,7 @@ def test_variable_context_still_takes_priority_inside_tcl_command(
|
||||
assert "localValue" in {item.label for item in items}
|
||||
|
||||
|
||||
def test_lsp_completion_returns_only_matching_command_options(
|
||||
tmp_path: Path, monkeypatch
|
||||
):
|
||||
def test_lsp_completion_returns_only_matching_command_options(tmp_path: Path, monkeypatch):
|
||||
_, current, _ = _completion_server(tmp_path, monkeypatch)
|
||||
source = "string compare "
|
||||
current = _document(tmp_path / "current.tcl", source)
|
||||
@@ -459,9 +419,7 @@ def test_lsp_completion_returns_only_matching_command_options(
|
||||
assert all(item.sort_text.startswith("000:") for item in items)
|
||||
|
||||
|
||||
def test_space_trigger_does_not_open_broad_fallback_completion(
|
||||
tmp_path: Path, monkeypatch
|
||||
):
|
||||
def test_space_trigger_does_not_open_broad_fallback_completion(tmp_path: Path, monkeypatch):
|
||||
_, current, _ = _completion_server(tmp_path, monkeypatch)
|
||||
source = "set value "
|
||||
current = _document(tmp_path / "current.tcl", source)
|
||||
@@ -550,9 +508,7 @@ def test_path_completion_is_relative_filtered_and_tcl_safe(tmp_path: Path):
|
||||
assert items[0].text_edit.range.start.character == len("source ")
|
||||
|
||||
|
||||
def test_lsp_source_completion_reads_paths_from_document_directory(
|
||||
tmp_path: Path, monkeypatch
|
||||
):
|
||||
def test_lsp_source_completion_reads_paths_from_document_directory(tmp_path: Path, monkeypatch):
|
||||
server, current, _ = _completion_server(tmp_path, monkeypatch)
|
||||
scripts = tmp_path / "scripts"
|
||||
scripts.mkdir()
|
||||
@@ -576,9 +532,7 @@ def test_lsp_source_completion_reads_paths_from_document_directory(
|
||||
assert "scripts/ignored.txt" not in labels
|
||||
|
||||
|
||||
def test_command_and_dict_for_snippets_use_lsp_snippet_placeholders(
|
||||
tmp_path: Path, monkeypatch
|
||||
):
|
||||
def test_command_and_dict_for_snippets_use_lsp_snippet_placeholders(tmp_path: Path, monkeypatch):
|
||||
_, current, source = _completion_server(tmp_path, monkeypatch)
|
||||
command_items = _complete(current, _position_after(source, "localP", occurrence=1))
|
||||
command_by_label = {item.label: item for item in command_items}
|
||||
@@ -595,14 +549,10 @@ def test_command_and_dict_for_snippets_use_lsp_snippet_placeholders(
|
||||
|
||||
switch_arguments = _argument_completion_request("switch ")
|
||||
assert switch_arguments is not None
|
||||
assert {"switch block", "-exact", "-glob", "-regexp"} <= {
|
||||
item.label for item in switch_arguments.items
|
||||
}
|
||||
assert {"switch block", "-exact", "-glob", "-regexp"} <= {item.label for item in switch_arguments.items}
|
||||
|
||||
|
||||
def test_semantic_variable_and_procedure_argument_completion(
|
||||
tmp_path: Path, monkeypatch
|
||||
):
|
||||
def test_semantic_variable_and_procedure_argument_completion(tmp_path: Path, monkeypatch):
|
||||
_, current, source = _completion_server(tmp_path, monkeypatch)
|
||||
|
||||
variable_items = _complete(current, _position_after(source, " set "))
|
||||
@@ -633,9 +583,7 @@ def test_semantic_variable_and_procedure_argument_completion(
|
||||
assert "string" not in procedure_labels
|
||||
|
||||
|
||||
def test_namespace_argument_completion_uses_navigation_index(
|
||||
tmp_path: Path, monkeypatch
|
||||
):
|
||||
def test_namespace_argument_completion_uses_navigation_index(tmp_path: Path, monkeypatch):
|
||||
server, current, _ = _completion_server(tmp_path, monkeypatch)
|
||||
namespace_source = "namespace eval tools { proc helper {} { return } }\n"
|
||||
namespace_document = _document(tmp_path / "namespaces.tcl", namespace_source)
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
"""Go to Definition, hover, references and rename between Tcl and .def files."""
|
||||
|
||||
from collections import namedtuple
|
||||
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 ADDRESS, BLOCK_TEMPLATE, FORMAT, parse_def_document
|
||||
|
||||
PSC = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Configuration>
|
||||
<Sourcing><Sequence>
|
||||
<Layer Name="Service" SubFolder="service">
|
||||
<DefinedEvents><Filename Name="service"/></DefinedEvents>
|
||||
</Layer>
|
||||
</Sequence></Sourcing>
|
||||
</Configuration>
|
||||
"""
|
||||
|
||||
DEF = """MACHINE Default
|
||||
|
||||
FORMATTING
|
||||
{
|
||||
FORMAT Coordinate "&__4.3_"
|
||||
ADDRESS SPOS
|
||||
{
|
||||
FORMAT Coordinate
|
||||
FORCE off
|
||||
MAX 99999.999 Abort
|
||||
MIN -99999.999 Abort
|
||||
LEADER "SPOS="
|
||||
}
|
||||
# ADDRESS commented_out
|
||||
BLOCK_TEMPLATE steady_rest
|
||||
{
|
||||
SPOS[$mom_pos(0)]
|
||||
Text[M60]\\opt
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
TCL = """proc MOM_steady {} {
|
||||
MOM_do_template steady_rest
|
||||
MOM_force Once SPOS X
|
||||
MOM_ask_address_value "SPOS"
|
||||
set name steady_rest
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def _project(tmp_path: Path, monkeypatch):
|
||||
(tmp_path / "service").mkdir()
|
||||
(tmp_path / "post.psc").write_text(PSC, encoding="utf-8")
|
||||
def_file = tmp_path / "service" / "service.def"
|
||||
def_file.write_text(DEF, encoding="utf-8")
|
||||
tcl_file = tmp_path / "caller.tcl"
|
||||
tcl_file.write_text(TCL, encoding="utf-8")
|
||||
|
||||
server = TclLanguageServer(name="def-navigation-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=tcl_file.as_uri(), language_id="tcl", version=1, text=TCL)
|
||||
)
|
||||
server.update_poco_completion_for_file(server.workspace.get_text_document(tcl_file.as_uri()))
|
||||
return server, def_file, tcl_file
|
||||
|
||||
|
||||
def _position(source: str, needle: str, occurrence: int = 0, offset: int = 1) -> lsp.Position:
|
||||
index = -1
|
||||
for _ in range(occurrence + 1):
|
||||
index = source.index(needle, index + 1)
|
||||
line = source.count("\n", 0, index)
|
||||
column = index - (source.rfind("\n", 0, index) + 1)
|
||||
return lsp.Position(line=line, character=column + offset)
|
||||
|
||||
|
||||
def _tcl_params(tcl_file: Path, needle: str, occurrence: int = 0):
|
||||
return lsp.TextDocumentIdentifier(uri=tcl_file.as_uri()), _position(TCL, needle, occurrence)
|
||||
|
||||
|
||||
# The client sends custom .def requests as plain JSON; pygls exposes them as namedtuples.
|
||||
_Doc = namedtuple("Object", ["uri"])
|
||||
_Pos = namedtuple("Object", ["line", "character"])
|
||||
_Params = namedtuple("Object", ["textDocument", "position", "text", "includeDeclaration", "newName"])
|
||||
|
||||
|
||||
def _def_params(def_file: Path, needle: str, occurrence: int = 0, text: str = DEF, offset: int = 1, **extra):
|
||||
position = _position(text, needle, occurrence, offset)
|
||||
return _Params(
|
||||
_Doc(def_file.as_uri()),
|
||||
_Pos(position.line, position.character),
|
||||
text,
|
||||
extra.get("includeDeclaration", True),
|
||||
extra.get("newName", ""),
|
||||
)
|
||||
|
||||
|
||||
def _lines(locations):
|
||||
return sorted((Path(location.uri).name, location.range.start.line, location.range.start.character) for location in locations)
|
||||
|
||||
|
||||
def test_parse_def_document_declarations_and_references():
|
||||
document = parse_def_document(DEF)
|
||||
kinds = [(item.kind, item.name) for item in document.declarations]
|
||||
assert kinds == [(FORMAT, "Coordinate"), (ADDRESS, "SPOS"), (BLOCK_TEMPLATE, "steady_rest")]
|
||||
address = document.declarations[1]
|
||||
assert (address.line, address.start, address.end) == (5, 12, 16)
|
||||
assert dict(address.properties)["LEADER"] == '"SPOS="'
|
||||
assert [(ref.name, ref.line, ref.container) for ref in document.references] == [
|
||||
("SPOS", 16, "steady_rest"),
|
||||
("Text", 17, "steady_rest"),
|
||||
]
|
||||
assert document.declarations[2].text.splitlines()[-1].strip() == "}"
|
||||
|
||||
|
||||
def test_tcl_goto_definition_of_template_and_address(tmp_path, monkeypatch):
|
||||
_, def_file, tcl_file = _project(tmp_path, monkeypatch)
|
||||
document, position = _tcl_params(tcl_file, "steady_rest")
|
||||
result = lsp_server.goto_definition(lsp.DefinitionParams(text_document=document, position=position))
|
||||
assert _lines(result) == [("service.def", 14, 19)]
|
||||
|
||||
for needle, occurrence in (("SPOS", 0), ("SPOS", 1)):
|
||||
document, position = _tcl_params(tcl_file, needle, occurrence)
|
||||
result = lsp_server.goto_definition(lsp.DefinitionParams(text_document=document, position=position))
|
||||
assert _lines(result) == [("service.def", 5, 12)]
|
||||
|
||||
|
||||
def test_tcl_goto_definition_ignores_plain_words_and_unknown_names(tmp_path, monkeypatch):
|
||||
_, _, tcl_file = _project(tmp_path, monkeypatch)
|
||||
# "set name steady_rest" is no template argument.
|
||||
document, position = _tcl_params(tcl_file, "steady_rest", 1)
|
||||
assert lsp_server.goto_definition(lsp.DefinitionParams(text_document=document, position=position)) is None
|
||||
document, position = lsp.TextDocumentIdentifier(uri=tcl_file.as_uri()), _position(TCL, "SPOS X", offset=5)
|
||||
assert lsp_server.goto_definition(lsp.DefinitionParams(text_document=document, position=position)) is None
|
||||
|
||||
|
||||
def test_tcl_hover_shows_template_body_and_address_properties(tmp_path, monkeypatch):
|
||||
_, _, tcl_file = _project(tmp_path, monkeypatch)
|
||||
document, position = _tcl_params(tcl_file, "steady_rest")
|
||||
hover = lsp_server.hover(lsp.HoverParams(text_document=document, position=position))
|
||||
assert "Block template" in hover.contents.value
|
||||
assert "SPOS[$mom_pos(0)]" in hover.contents.value
|
||||
assert "```def" in hover.contents.value
|
||||
|
||||
document, position = _tcl_params(tcl_file, "SPOS")
|
||||
value = lsp_server.hover(lsp.HoverParams(text_document=document, position=position)).contents.value
|
||||
assert "| Format | `Coordinate` → `\"&__4.3_\"` |" in value
|
||||
assert '| Leader | `"SPOS="` |' in value
|
||||
assert "| Min | `-99999.999 Abort` |" in value
|
||||
assert "| Max | `99999.999 Abort` |" in value
|
||||
assert "| Modality | `off` (modal, output only on change) |" in value
|
||||
|
||||
|
||||
def test_tcl_references_include_def_declaration_and_template_elements(tmp_path, monkeypatch):
|
||||
_, _, tcl_file = _project(tmp_path, monkeypatch)
|
||||
document, position = _tcl_params(tcl_file, "SPOS")
|
||||
result = lsp_server.references(
|
||||
lsp.ReferenceParams(
|
||||
text_document=document,
|
||||
position=position,
|
||||
context=lsp.ReferenceContext(include_declaration=True),
|
||||
)
|
||||
)
|
||||
assert _lines(result) == [
|
||||
("caller.tcl", 2, 19),
|
||||
("caller.tcl", 3, 27),
|
||||
("service.def", 5, 12),
|
||||
("service.def", 16, 8),
|
||||
]
|
||||
result = lsp_server.references(
|
||||
lsp.ReferenceParams(
|
||||
text_document=document,
|
||||
position=position,
|
||||
context=lsp.ReferenceContext(include_declaration=False),
|
||||
)
|
||||
)
|
||||
assert ("service.def", 5, 12) not in _lines(result)
|
||||
|
||||
|
||||
def test_tcl_rename_updates_def_and_tcl(tmp_path, monkeypatch):
|
||||
_, def_file, tcl_file = _project(tmp_path, monkeypatch)
|
||||
document, position = _tcl_params(tcl_file, "SPOS")
|
||||
prepared = lsp_server.prepare_rename(lsp.PrepareRenameParams(text_document=document, position=position))
|
||||
assert prepared.placeholder == "SPOS"
|
||||
|
||||
edit = lsp_server.rename(lsp.RenameParams(text_document=document, position=position, new_name="STEADY_POS"))
|
||||
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": [(3, 27), (2, 19)], "service.def": [(16, 8), (5, 12)]}
|
||||
assert all(e.new_text == "STEADY_POS" for items in edit.changes.values() for e in items)
|
||||
|
||||
|
||||
def test_undeclared_names_are_not_renamed(tmp_path, monkeypatch):
|
||||
_, _, tcl_file = _project(tmp_path, monkeypatch)
|
||||
document = lsp.TextDocumentIdentifier(uri=tcl_file.as_uri())
|
||||
position = _position(TCL, "SPOS X", offset=5)
|
||||
assert lsp_server.prepare_rename(lsp.PrepareRenameParams(text_document=document, position=position)) is None
|
||||
|
||||
|
||||
def test_def_requests_resolve_declarations_and_elements(tmp_path, monkeypatch):
|
||||
_, def_file, _ = _project(tmp_path, monkeypatch)
|
||||
# Address element inside a block template -> ADDRESS declaration.
|
||||
result = lsp_server.def_definition(_def_params(def_file, "SPOS[", offset=1))
|
||||
assert _lines(result) == [("service.def", 5, 12)]
|
||||
|
||||
hover = lsp_server.def_hover(_def_params(def_file, "steady_rest"))
|
||||
assert "Text[M60]" in hover.contents.value
|
||||
|
||||
references = lsp_server.def_references(_def_params(def_file, "ADDRESS SPOS", offset=9))
|
||||
assert _lines(references) == [
|
||||
("caller.tcl", 2, 19),
|
||||
("caller.tcl", 3, 27),
|
||||
("service.def", 5, 12),
|
||||
("service.def", 16, 8),
|
||||
]
|
||||
assert lsp_server.def_hover(_def_params(def_file, "MACHINE")) is None
|
||||
|
||||
|
||||
def test_def_requests_use_unsaved_text(tmp_path, monkeypatch):
|
||||
_, def_file, _ = _project(tmp_path, monkeypatch)
|
||||
text = DEF.replace("BLOCK_TEMPLATE steady_rest", "BLOCK_TEMPLATE steady_rest_new")
|
||||
params = _def_params(def_file, "steady_rest_new", text=text, newName="rest")
|
||||
assert lsp_server.def_prepare_rename(params).placeholder == "steady_rest_new"
|
||||
edit = lsp_server.def_rename(params)
|
||||
assert list(edit.changes) == [def_file.as_uri()]
|
||||
|
||||
|
||||
def test_def_rename_updates_tcl_callers(tmp_path, monkeypatch):
|
||||
_, def_file, _ = _project(tmp_path, monkeypatch)
|
||||
edit = lsp_server.def_rename(_def_params(def_file, "steady_rest", newName="lunette"))
|
||||
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
|
||||
Reference in New Issue
Block a user