feat(tcl): add static variable name extraction and array key completion
This adds tooling to statically extract Tcl variable names from syntax trees without evaluating substitutions, enabling better completions for array elements and plain variables. The new helpers are wired into the completion and navigation flows and are supported by tests covering array keys and substitutions. - Introduce variable_names.py with variable_name() and array_key_parts() - Wire static name extraction into completion and symbol indexing - Add tests for array key completion with substitutions
This commit is contained in:
@@ -103,6 +103,133 @@ def _argument_completion_labels(source: str) -> set[str] | None:
|
||||
return {item.label for item in completion.items}
|
||||
|
||||
|
||||
def test_array_keys_complete_in_set_and_substitution(tmp_path: Path, monkeypatch):
|
||||
server, _, _ = _completion_server(tmp_path, monkeypatch)
|
||||
workspace = _document(
|
||||
tmp_path / "arrays.tcl",
|
||||
"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"
|
||||
)
|
||||
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,
|
||||
))
|
||||
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"]
|
||||
assert all(item.text_edit.new_text == item.label for item in items)
|
||||
items = _complete(current, _position_after(source, "puts $lib_flag(en"))
|
||||
assert [item.label for item in items] == ["enabled"]
|
||||
assert items[0].text_edit.new_text == "enabled"
|
||||
position = lsp.Position(line=6, character=len(source.splitlines()[6].encode("utf-16-le")) // 2)
|
||||
items = _complete(current, position)
|
||||
assert [item.label for item in items] == ["empty"]
|
||||
assert items[0].text_edit.new_text == "empty)"
|
||||
assert items[0].text_edit.range.start.character == position.character - 2
|
||||
|
||||
|
||||
def test_dynamic_array_index_keeps_variable_identity(tmp_path: Path, monkeypatch):
|
||||
from tools.navigation import build_file_symbol_index
|
||||
from tools.parser import CustomParser
|
||||
from tools.semantic_tokens import _Highlighter
|
||||
from tools.variable_index import build_variable_index
|
||||
from tools.variable_names import variable_name
|
||||
|
||||
source = (
|
||||
"set custom_flag(from_move,$::mom_path_name) 1\n"
|
||||
'set "::quoted_flag(from_move,$::mom_path_name)" 1\n'
|
||||
"set ::command_flag([info hostname]) 1\n"
|
||||
"set ${dynamic_name}(entry) 1\n"
|
||||
"proc example {} { set local_flag($::mom_path_name) 1 }\n"
|
||||
"puts $custom_flag\n"
|
||||
)
|
||||
tree = CustomParser().parse(source)
|
||||
globals_, locals_, _ = build_variable_index(source, tree)
|
||||
assert {"custom_flag", "quoted_flag", "command_flag"} <= globals_
|
||||
assert locals_["example"] == {"local_flag"}
|
||||
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
|
||||
)
|
||||
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
|
||||
)
|
||||
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,
|
||||
))
|
||||
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))
|
||||
)
|
||||
assert {"quoted_flag", "command_flag"} <= {item.label for item in workspace_items}
|
||||
|
||||
|
||||
def test_literal_array_components_complete_around_substitutions(tmp_path: Path, monkeypatch):
|
||||
from tools.completion_items import array_element_completions
|
||||
|
||||
server, _, _ = _completion_server(tmp_path, monkeypatch)
|
||||
source = (
|
||||
"set custom_flag(from_move,$::mom_path_name) 1\n"
|
||||
"set custom_flag(to_move,$::mom_path_name) 1\n"
|
||||
"set custom_flag($::mom_path_name,finished) 1\n"
|
||||
"set custom_flag(prefix_$::mom_path_name,other) 1\n"
|
||||
"set custom_flag([info hostname],command_tail) 1\n"
|
||||
"set other_flag(wrong,$::mom_path_name) 1\n"
|
||||
"set multi_flag(move,$first,axis,$second) 1\n"
|
||||
)
|
||||
document = _document(tmp_path / "components.tcl", source)
|
||||
assert server.update_poco_completion_for_file(document)
|
||||
cases = [
|
||||
("set custom_flag(|,$::mom_path_name)", {"from_move", "to_move"}, "from_move", "set custom_flag(from_move,$::mom_path_name)"),
|
||||
("set custom_flag(fr|om_old,$::mom_path_name)", {"from_move"}, "from_move", "set custom_flag(from_move,$::mom_path_name)"),
|
||||
("puts $custom_flag($::mom_path_name,fi|)", {"finished"}, "finished", "puts $custom_flag($::mom_path_name,finished)"),
|
||||
("set custom_flag($::mom_path_name,|)", {"finished", "other", "command_tail"}, "other", "set custom_flag($::mom_path_name,other)"),
|
||||
("set custom_flag(fr|", {"from_move"}, "from_move", "set custom_flag(from_move,$::mom_path_name)"),
|
||||
("set custom_flag(fr|)", {"from_move"}, "from_move", "set custom_flag(from_move,$::mom_path_name)"),
|
||||
("set multi_flag(m|)", {"move"}, "move", "set multi_flag(move,$first,axis,$second)"),
|
||||
]
|
||||
for marked, labels, selected, expected in cases:
|
||||
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"),
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
def _argument_completion_request(source: str):
|
||||
lines = source.split("\n")
|
||||
character = len(lines[-1].encode("utf-16-le")) // 2
|
||||
|
||||
Reference in New Issue
Block a user