Files
nx_post_support/server/src/tools/completion_items.py
T
Christoph Brandau af5acfc946 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.
2026-09-03 10:35:39 +02:00

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))