feat(tcl): add dynamic argument completion and snippets

The changes add dynamic, semantic argument completion for Tcl
commands and snippet support.

- Introduces DynamicCompletionKind, TclArgumentCompletion, and dynamic rules
  for Tcl to provide variable, procedure, namespace, and path suggestions.
- Adds snippet-backed commands and arguments for Tcl blocks and paths.
- Refactors tcl_argument_completion and updates the LSP to use dynamic path,
  variable, and namespace completions with snippet kinds.
This commit is contained in:
Christoph Brandau
2026-09-03 10:35:39 +02:00
parent 20f76b6a20
commit af5acfc946
7 changed files with 781 additions and 56 deletions
@@ -19,7 +19,11 @@ from tools.completion_items import (
CompletionContext,
completion_context,
)
from tools.tcl_command_completion import tcl_argument_completions
from tools.tcl_command_completion import (
DynamicCompletionKind,
path_completion_items,
tcl_argument_completion,
)
def _position_after(source: str, token: str, occurrence: int = 0) -> lsp.Position:
@@ -93,15 +97,19 @@ def _complete(document: TextDocument, position: lsp.Position):
def _argument_completion_labels(source: str) -> set[str] | None:
completion = _argument_completion_request(source)
if completion is None:
return None
return {item.label for item in completion.items}
def _argument_completion_request(source: str):
lines = source.split("\n")
character = len(lines[-1].encode("utf-16-le")) // 2
items = tcl_argument_completions(
return tcl_argument_completion(
lines,
lsp.Position(line=len(lines) - 1, character=character),
)
if items is None:
return None
return {item.label for item in items}
def test_variable_completion_filters_and_ranks_candidates(tmp_path: Path, monkeypatch):
@@ -308,3 +316,172 @@ def test_space_trigger_does_not_open_broad_fallback_completion(
)
assert result.items == []
def test_argument_values_are_suggested_at_the_expected_position():
assert _argument_completion_labels("return -code ") == {
"break",
"continue",
"error",
"ok",
"return",
}
assert _argument_completion_labels("open output.txt ") == {
"a",
"a+",
"r",
"r+",
"w",
"w+",
}
assert _argument_completion_labels("seek channel 0 ") == {
"current",
"end",
"start",
}
def test_dynamic_argument_categories_are_detected():
variable = _argument_completion_request("set ")
procedure = _argument_completion_request("info body ")
namespace = _argument_completion_request("namespace eval ")
path = _argument_completion_request("source scripts/")
assert variable is not None
assert variable.dynamic_kind == DynamicCompletionKind.VARIABLE
assert procedure is not None
assert procedure.dynamic_kind == DynamicCompletionKind.PROCEDURE
assert namespace is not None
assert namespace.dynamic_kind == DynamicCompletionKind.NAMESPACE
assert path is not None
assert path.dynamic_kind == DynamicCompletionKind.PATH
assert path.path_extensions == (".tcl",)
def test_path_completion_is_relative_filtered_and_tcl_safe(tmp_path: Path):
scripts = tmp_path / "scripts"
scripts.mkdir()
(scripts / "first file.tcl").write_text("puts ok", encoding="utf-8")
(scripts / "ignored.txt").write_text("ignored", encoding="utf-8")
source = "source scripts/f"
completion = _argument_completion_request(source)
assert completion is not None
items = path_completion_items(
tmp_path,
completion,
_position_after(source, source),
)
assert [item.label for item in items] == ["scripts/first\\ file.tcl"]
assert items[0].kind == lsp.CompletionItemKind.File
assert items[0].text_edit is not None
assert items[0].text_edit.new_text == "scripts/first\\ file.tcl"
assert items[0].text_edit.range.start.character == len("source ")
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()
(scripts / "helper.tcl").write_text("proc helper {} {}", encoding="utf-8")
(scripts / "ignored.txt").write_text("ignored", encoding="utf-8")
source = "source scripts/"
current = _document(tmp_path / "current.tcl", source)
server.workspace.put_text_document(
lsp.TextDocumentItem(
uri=current.uri,
language_id="tcl",
version=2,
text=source,
)
)
items = _complete(current, _position_after(source, source))
labels = {item.label for item in items}
assert "scripts/helper.tcl" in labels
assert "scripts/ignored.txt" not in labels
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}
assert command_by_label["if"].kind == lsp.CompletionItemKind.Snippet
assert command_by_label["if"].insert_text_format == lsp.InsertTextFormat.Snippet
assert "${1:condition}" in (command_by_label["if"].insert_text or "")
dict_for = _argument_completion_request("dict f")
assert dict_for is not None
for_item = next(item for item in dict_for.items if item.label == "for")
assert for_item.kind == lsp.CompletionItemKind.Snippet
assert "${3:dictionary}" in (for_item.insert_text or "")
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
}
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 "))
assert variable_items
assert all(item.kind in VARIABLE_KINDS for item in variable_items)
assert {"argument", "localValue"} <= {item.label for item in variable_items}
procedure_source = source + "info body localProc\n"
current = _document(tmp_path / "current.tcl", procedure_source)
lsp_server.LSP_SERVER.workspace.put_text_document(
lsp.TextDocumentItem(
uri=current.uri,
language_id="tcl",
version=2,
text=procedure_source,
)
)
procedure_items = _complete(
current,
_position_after(procedure_source, "info body "),
)
procedure_labels = {item.label for item in procedure_items}
assert "localProc" in procedure_labels
assert "workspaceProc" in procedure_labels
assert "MOM_abort" in procedure_labels
assert "localValue" not in procedure_labels
assert "string" not in procedure_labels
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)
assert server.update_poco_completion_for_file(namespace_document)
source = "namespace eval "
current = _document(tmp_path / "current.tcl", source)
server.workspace.put_text_document(
lsp.TextDocumentItem(
uri=current.uri,
language_id="tcl",
version=2,
text=source,
)
)
items = _complete(current, _position_after(source, source))
assert "tools" in {item.label for item in items}
assert all(item.kind == lsp.CompletionItemKind.Module for item in items)