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.
177 lines
6.0 KiB
Python
177 lines
6.0 KiB
Python
from __future__ import annotations
|
|
|
|
import copy
|
|
import re
|
|
from collections.abc import Iterable, Sequence
|
|
from enum import Enum
|
|
|
|
import lsprotocol.types as lsp
|
|
from tclint.syntax_tree import BareWord, Command, List, Visitor
|
|
|
|
from common.load_data import standard_items
|
|
from tools.tcl_command_completion import line_prefix_at_position
|
|
|
|
BUILTIN_VAR_LABELS = {ci.label for ci in standard_items.nx_variables}
|
|
BUILTIN_PROC_LABELS = {ci.label for ci in standard_items.nx_procs}
|
|
|
|
|
|
class CompletionContext(Enum):
|
|
VARIABLE = "variable"
|
|
COMMAND = "command"
|
|
GENERAL = "general"
|
|
|
|
|
|
VARIABLE_KINDS = {
|
|
lsp.CompletionItemKind.Variable,
|
|
lsp.CompletionItemKind.Field,
|
|
lsp.CompletionItemKind.Constant,
|
|
}
|
|
COMMAND_KINDS = {
|
|
lsp.CompletionItemKind.Function,
|
|
lsp.CompletionItemKind.Method,
|
|
lsp.CompletionItemKind.Constructor,
|
|
lsp.CompletionItemKind.Keyword,
|
|
lsp.CompletionItemKind.Snippet,
|
|
}
|
|
|
|
_VARIABLE_PREFIX_RE = re.compile(r"(?<!\\)\$(?:\{)?[A-Za-z0-9_:]*$")
|
|
_COMMAND_PREFIX_RE = re.compile(r"(?:^|[;\[\{])\s*[^\s;\[\]\{\}]*$")
|
|
|
|
|
|
def completion_context(
|
|
source_lines: Sequence[str], position: lsp.Position
|
|
) -> CompletionContext:
|
|
prefix = line_prefix_at_position(source_lines, position)
|
|
if prefix is None:
|
|
return CompletionContext.GENERAL
|
|
if _VARIABLE_PREFIX_RE.search(prefix):
|
|
return CompletionContext.VARIABLE
|
|
if _COMMAND_PREFIX_RE.search(prefix):
|
|
return CompletionContext.COMMAND
|
|
return CompletionContext.GENERAL
|
|
|
|
|
|
def _is_allowed(item: lsp.CompletionItem, context: CompletionContext) -> bool:
|
|
if context == CompletionContext.VARIABLE:
|
|
return item.kind in VARIABLE_KINDS
|
|
if context == CompletionContext.COMMAND:
|
|
return item.kind in COMMAND_KINDS
|
|
return True
|
|
|
|
|
|
def ranked_completion_items(
|
|
candidates: Iterable[tuple[int, lsp.CompletionItem]],
|
|
context: CompletionContext,
|
|
) -> list[lsp.CompletionItem]:
|
|
"""Filter, de-duplicate, and rank completion candidates for one request."""
|
|
items = []
|
|
seen: set[tuple[str, lsp.CompletionItemKind | None]] = set()
|
|
for sequence, (priority, item) in enumerate(candidates):
|
|
if not _is_allowed(item, context):
|
|
continue
|
|
|
|
key = (item.label, item.kind)
|
|
if key in seen:
|
|
continue
|
|
seen.add(key)
|
|
|
|
ranked = copy.copy(item)
|
|
ranked.sort_text = f"{priority:03d}:{item.label.casefold()}:{sequence:06d}"
|
|
items.append(ranked)
|
|
return items
|
|
|
|
|
|
class CompletionItems:
|
|
def __init__(self):
|
|
self._custom_functions: list[lsp.CompletionItem] = []
|
|
|
|
@property
|
|
def custom_functions(self) -> list[lsp.CompletionItem]:
|
|
return self._custom_functions
|
|
|
|
@custom_functions.setter
|
|
def custom_functions(self, value: lsp.CompletionItem):
|
|
self._custom_functions.append(value)
|
|
|
|
|
|
class CompletionCollector(Visitor):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self._custom_functions: list[lsp.CompletionItem] = []
|
|
self._custom_function_keys: set[tuple[str, lsp.CompletionItemKind | None]] = set()
|
|
self._proc_signatures = {}
|
|
|
|
@property
|
|
def custom_functions(self) -> list[lsp.CompletionItem]:
|
|
return self._custom_functions
|
|
|
|
@property
|
|
def proc_signatures(self):
|
|
return self._proc_signatures
|
|
|
|
def _append_unique(self, item: lsp.CompletionItem):
|
|
# Avoid duplicate labels within the same file scan
|
|
key = (item.label, item.kind)
|
|
if key in self._custom_function_keys:
|
|
return
|
|
self._custom_function_keys.add(key)
|
|
self._custom_functions.append(item)
|
|
|
|
def visit_command(self, command: Command):
|
|
routine = command.routine
|
|
|
|
# Collect custom proc names and their signatures
|
|
if routine.contents == "proc" and command.args:
|
|
first_arg = command.args[0]
|
|
if not getattr(first_arg, "value", None):
|
|
return
|
|
|
|
if first_arg.value in BUILTIN_PROC_LABELS:
|
|
return
|
|
|
|
# Record proc name as a completion item
|
|
self._append_unique(lsp.CompletionItem(label=first_arg.value, kind=lsp.CompletionItemKind.Function))
|
|
if len(command.args) < 2:
|
|
return
|
|
|
|
param_list_node = command.args[1]
|
|
if not hasattr(param_list_node, "children"):
|
|
return
|
|
|
|
param_names = []
|
|
for arg in param_list_node.children:
|
|
if isinstance(arg, BareWord):
|
|
param_names.append(arg.value)
|
|
elif isinstance(arg, List) and len(arg.children) >= 1:
|
|
first = arg.children[0]
|
|
if isinstance(first, BareWord):
|
|
param_names.append(first.value)
|
|
|
|
self._proc_signatures[first_arg.value] = param_names
|
|
|
|
# Collect global variables declared with: global var1 var2 ...
|
|
elif routine.contents == "global" and command.args:
|
|
for arg in command.args:
|
|
if (
|
|
isinstance(arg, BareWord)
|
|
and getattr(arg, "value", None)
|
|
and arg.value not in BUILTIN_VAR_LABELS
|
|
):
|
|
self._append_unique(
|
|
lsp.CompletionItem(
|
|
label=arg.value,
|
|
kind=lsp.CompletionItemKind.Variable,
|
|
)
|
|
)
|
|
|
|
# Collect variables set with explicit global namespace: set ::var_name ...
|
|
elif routine.contents == "set" and command.args:
|
|
first = command.args[0]
|
|
if isinstance(first, BareWord) and getattr(first, "value", None):
|
|
var_name = first.value
|
|
if var_name.startswith("::"):
|
|
base_name = var_name.split("(", 1)[0]
|
|
clean_name = base_name[2:] # remove leading '::' for completion display
|
|
if clean_name not in BUILTIN_VAR_LABELS:
|
|
self._append_unique(lsp.CompletionItem(label=clean_name, kind=lsp.CompletionItemKind.Variable))
|