feat(lsp): add Tcl command-aware completion and integration
- Introduced a Tcl command-aware completion engine and wired to LSP. - Added a new module with Tcl commands, options, and contextual matching. - Updated completion to favor command-aware items and args.
This commit is contained in:
@@ -6,18 +6,20 @@ SRC_DIR = THIS_DIR.parent.parent / "src"
|
||||
if str(SRC_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(SRC_DIR))
|
||||
|
||||
import lsp_server
|
||||
import lsprotocol.types as lsp # type: ignore
|
||||
from common.load_data import standard_items
|
||||
from lsp_tclserver import TclLanguageServer
|
||||
from pygls.workspace import Workspace
|
||||
from pygls.workspace.text_document import TextDocument
|
||||
|
||||
import lsp_server
|
||||
from common.load_data import standard_items
|
||||
from lsp_tclserver import TclLanguageServer
|
||||
from tools.completion_items import (
|
||||
COMMAND_KINDS,
|
||||
VARIABLE_KINDS,
|
||||
CompletionContext,
|
||||
completion_context,
|
||||
)
|
||||
from tools.tcl_command_completion import tcl_argument_completions
|
||||
|
||||
|
||||
def _position_after(source: str, token: str, occurrence: int = 0) -> lsp.Position:
|
||||
@@ -90,6 +92,18 @@ def _complete(document: TextDocument, position: lsp.Position):
|
||||
).items
|
||||
|
||||
|
||||
def _argument_completion_labels(source: str) -> set[str] | None:
|
||||
lines = source.split("\n")
|
||||
character = len(lines[-1].encode("utf-16-le")) // 2
|
||||
items = tcl_argument_completions(
|
||||
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):
|
||||
_, current, source = _completion_server(tmp_path, monkeypatch)
|
||||
items = _complete(current, _position_after(source, "$local"))
|
||||
@@ -150,3 +164,147 @@ def test_completion_context_handles_nested_commands_and_utf16():
|
||||
completion_context(["puts value"], lsp.Position(line=0, character=10))
|
||||
== CompletionContext.GENERAL
|
||||
)
|
||||
|
||||
|
||||
def test_string_subcommands_and_compare_options_are_context_aware():
|
||||
subcommands = _argument_completion_labels("string ")
|
||||
assert subcommands is not None
|
||||
assert {"compare", "equal", "is", "map", "match"} <= subcommands
|
||||
|
||||
assert _argument_completion_labels("string compare ") == {
|
||||
"-length",
|
||||
"-nocase",
|
||||
}
|
||||
assert _argument_completion_labels("string compare -nocase ") == {
|
||||
"-length"
|
||||
}
|
||||
assert _argument_completion_labels("string compare -length ") is None
|
||||
|
||||
|
||||
def test_string_completion_handles_nested_commands_and_is_values():
|
||||
assert _argument_completion_labels("set result [string compare -") == {
|
||||
"-length",
|
||||
"-nocase",
|
||||
}
|
||||
|
||||
classes = _argument_completion_labels("string is ")
|
||||
assert classes is not None
|
||||
assert {"boolean", "double", "integer", "wideinteger"} <= classes
|
||||
assert _argument_completion_labels("string is integer ") == {
|
||||
"-failindex",
|
||||
"-strict",
|
||||
}
|
||||
|
||||
|
||||
def test_dict_array_namespace_file_and_info_subcommands():
|
||||
dict_items = _argument_completion_labels("dict ")
|
||||
assert dict_items is not None
|
||||
assert {"create", "filter", "get", "set", "with"} <= dict_items
|
||||
assert _argument_completion_labels("dict filter ") == {
|
||||
"key",
|
||||
"script",
|
||||
"value",
|
||||
}
|
||||
|
||||
assert _argument_completion_labels("array names values ") == {
|
||||
"-exact",
|
||||
"-glob",
|
||||
"-regexp",
|
||||
}
|
||||
|
||||
namespace_items = _argument_completion_labels("namespace ")
|
||||
assert namespace_items is not None
|
||||
assert {"children", "ensemble", "eval", "which"} <= namespace_items
|
||||
assert _argument_completion_labels("namespace which ") == {
|
||||
"-command",
|
||||
"-variable",
|
||||
}
|
||||
assert _argument_completion_labels("namespace ensemble ") == {
|
||||
"configure",
|
||||
"create",
|
||||
"exists",
|
||||
}
|
||||
|
||||
file_items = _argument_completion_labels("file ")
|
||||
assert file_items is not None
|
||||
assert {"copy", "delete", "exists", "normalize", "rename"} <= file_items
|
||||
assert _argument_completion_labels("file copy ") == {"--", "-force"}
|
||||
|
||||
info_items = _argument_completion_labels("info ")
|
||||
assert info_items is not None
|
||||
assert {"args", "body", "commands", "exists", "procs", "vars"} <= info_items
|
||||
|
||||
|
||||
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",
|
||||
" puts $local\n string compare $localValue other\n",
|
||||
)
|
||||
current = _document(tmp_path / "current.tcl", command_source)
|
||||
lsp_server.LSP_SERVER.workspace.put_text_document(
|
||||
lsp.TextDocumentItem(
|
||||
uri=current.uri,
|
||||
language_id="tcl",
|
||||
version=2,
|
||||
text=command_source,
|
||||
)
|
||||
)
|
||||
items = _complete(current, _position_after(command_source, "$localValue"))
|
||||
|
||||
assert items
|
||||
assert all(item.kind in VARIABLE_KINDS for item in items)
|
||||
assert "localValue" in {item.label for item in items}
|
||||
|
||||
|
||||
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)
|
||||
lsp_server.LSP_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 {item.label for item in items} == {"-length", "-nocase"}
|
||||
assert all(item.kind == lsp.CompletionItemKind.Keyword for item in items)
|
||||
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
|
||||
):
|
||||
_, current, _ = _completion_server(tmp_path, monkeypatch)
|
||||
source = "set value "
|
||||
current = _document(tmp_path / "current.tcl", source)
|
||||
lsp_server.LSP_SERVER.workspace.put_text_document(
|
||||
lsp.TextDocumentItem(
|
||||
uri=current.uri,
|
||||
language_id="tcl",
|
||||
version=2,
|
||||
text=source,
|
||||
)
|
||||
)
|
||||
|
||||
result = lsp_server.on_completion(
|
||||
lsp.CompletionParams(
|
||||
text_document=lsp.TextDocumentIdentifier(uri=current.uri),
|
||||
position=_position_after(source, source),
|
||||
context=lsp.CompletionContext(
|
||||
trigger_kind=lsp.CompletionTriggerKind.TriggerCharacter,
|
||||
trigger_character=" ",
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
assert result.items == []
|
||||
|
||||
Reference in New Issue
Block a user