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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user