This commit is contained in:
@@ -125,10 +125,46 @@ def document_diagnostic(params: lsp.DocumentDiagnosticParams):
|
|||||||
|
|
||||||
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_COMPLETION)
|
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_COMPLETION)
|
||||||
def on_completion(params: lsp.CompletionParams) -> lsp.CompletionList:
|
def on_completion(params: lsp.CompletionParams) -> lsp.CompletionList:
|
||||||
_ = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
|
from tools.variable_index import build_variable_index
|
||||||
|
from tools.completion_items import BUILTIN_VAR_LABELS
|
||||||
|
|
||||||
|
doc = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
|
||||||
|
|
||||||
|
# Base items
|
||||||
poco = [item for items in LSP_SERVER.poco_completion.values() for item in items]
|
poco = [item for items in LSP_SERVER.poco_completion.values() for item in items]
|
||||||
items = standard_items.tcl_keyword_list + standard_items.nx_procs + standard_items.nx_variables + poco
|
base_items = standard_items.tcl_keyword_list + standard_items.nx_procs + standard_items.nx_variables + poco
|
||||||
return lsp.CompletionList(is_incomplete=False, items=items)
|
|
||||||
|
# Build variable index from current document
|
||||||
|
globals_set, procs_locals, proc_ranges = build_variable_index(doc.source)
|
||||||
|
|
||||||
|
# Always include globals (excluding built-ins)
|
||||||
|
dynamic_items = []
|
||||||
|
for name in sorted(globals_set):
|
||||||
|
if name not in BUILTIN_VAR_LABELS:
|
||||||
|
dynamic_items.append(lsp.CompletionItem(label=name, kind=lsp.CompletionItemKind.Variable))
|
||||||
|
|
||||||
|
# Include proc-local variables when cursor is inside that proc
|
||||||
|
pos = params.position
|
||||||
|
if pos is not None:
|
||||||
|
for pr in proc_ranges:
|
||||||
|
if pr.start_line <= pos.line <= (pr.end_line or pr.start_line):
|
||||||
|
for name in sorted(procs_locals.get(pr.name, set())):
|
||||||
|
# Exclude built-ins and globals to avoid duplication
|
||||||
|
if name not in BUILTIN_VAR_LABELS and name not in globals_set:
|
||||||
|
dynamic_items.append(lsp.CompletionItem(label=name, kind=lsp.CompletionItemKind.Variable))
|
||||||
|
break
|
||||||
|
|
||||||
|
# Merge with de-duplication for variables only
|
||||||
|
merged: list[lsp.CompletionItem] = []
|
||||||
|
seen_var_labels: set[str] = set()
|
||||||
|
for it in base_items + dynamic_items:
|
||||||
|
if getattr(it, "kind", None) == lsp.CompletionItemKind.Variable:
|
||||||
|
if it.label in seen_var_labels:
|
||||||
|
continue
|
||||||
|
seen_var_labels.add(it.label)
|
||||||
|
merged.append(it)
|
||||||
|
|
||||||
|
return lsp.CompletionList(is_incomplete=False, items=merged)
|
||||||
|
|
||||||
|
|
||||||
# @LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DOCUMENT_SYMBOL)
|
# @LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DOCUMENT_SYMBOL)
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ from tclint.syntax_tree import Visitor, Command, BareWord, List
|
|||||||
import lsprotocol.types as lsp
|
import lsprotocol.types as lsp
|
||||||
from common.load_data import standard_items
|
from common.load_data import standard_items
|
||||||
|
|
||||||
|
BUILTIN_VAR_LABELS = {ci.label for ci in standard_items.nx_variables}
|
||||||
|
|
||||||
|
|
||||||
class CompletionItems:
|
class CompletionItems:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -34,23 +36,25 @@ class _Completion(Visitor):
|
|||||||
self._custom_functions = []
|
self._custom_functions = []
|
||||||
self._proc_signatures = {}
|
self._proc_signatures = {}
|
||||||
|
|
||||||
|
def _append_unique(self, item: lsp.CompletionItem):
|
||||||
|
# Avoid duplicate labels within the same file scan
|
||||||
|
if not any(ci.label == item.label for ci in self._custom_functions):
|
||||||
|
self._custom_functions.append(item)
|
||||||
|
|
||||||
def visit_command(self, command: Command):
|
def visit_command(self, command: Command):
|
||||||
routine = command.routine
|
routine = command.routine
|
||||||
|
|
||||||
|
# Collect custom proc names and their signatures
|
||||||
if routine.contents == "proc" and command.args:
|
if routine.contents == "proc" and command.args:
|
||||||
first_arg = command.args[0]
|
first_arg = command.args[0]
|
||||||
if not first_arg.value:
|
if not getattr(first_arg, "value", None):
|
||||||
return
|
return
|
||||||
|
|
||||||
if any(item.label == first_arg.value for item in standard_items.nx_procs):
|
if any(item.label == first_arg.value for item in standard_items.nx_procs):
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
# Record proc name as a completion item
|
||||||
self._custom_functions.remove(first_arg.value)
|
self._append_unique(lsp.CompletionItem(label=first_arg.value, kind=lsp.CompletionItemKind.Function))
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
self._custom_functions.append(lsp.CompletionItem(label=first_arg.value, kind=lsp.CompletionItemKind.Function))
|
|
||||||
if len(command.args) < 2:
|
if len(command.args) < 2:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -69,6 +73,24 @@ class _Completion(Visitor):
|
|||||||
|
|
||||||
self._proc_signatures[first_arg.value] = param_names
|
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):
|
||||||
|
if 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))
|
||||||
|
|
||||||
|
|
||||||
def remove_existing_items(items: list[lsp.CompletionItem], store: dict) -> None:
|
def remove_existing_items(items: list[lsp.CompletionItem], store: dict) -> None:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import re
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Dict, Set, List, Tuple
|
||||||
|
|
||||||
|
# Reuse patterns similar to document_symbols
|
||||||
|
NS_RE = re.compile(r"^\s*namespace\s+eval\s+([^\s\{]+)")
|
||||||
|
PROC_RE = re.compile(r"^\s*proc\s+([^\s\{]+)\s+\{.*\}\s+\{")
|
||||||
|
SET_RE = re.compile(r"^\s*set\s+([^\s\}]+)")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ProcRange:
|
||||||
|
name: str
|
||||||
|
start_line: int
|
||||||
|
end_line: int | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def build_variable_index(source: str) -> tuple[Set[str], Dict[str, Set[str]], List[ProcRange]]:
|
||||||
|
"""
|
||||||
|
Parse Tcl source text and build:
|
||||||
|
- globals: set of variable names considered global suggestions
|
||||||
|
- procs: mapping proc_name -> set of local variable names (set without :: inside that proc)
|
||||||
|
- proc_ranges: list of ProcRange (name, start_line, end_line)
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- set ::var -> global var suggestion (strip leading :: and any array index "(")
|
||||||
|
- set var without :: at top level (not in namespace/proc) -> global suggestion
|
||||||
|
- set var without :: inside proc -> local to that proc
|
||||||
|
- set var inside namespace (no ::) is ignored for global suggestions
|
||||||
|
"""
|
||||||
|
lines = source.split("\n")
|
||||||
|
|
||||||
|
class Scope:
|
||||||
|
def __init__(self, name: str, kind: str, start_line: int):
|
||||||
|
self.name = name
|
||||||
|
self.kind = kind # "namespace" or "proc" or "root"
|
||||||
|
self.start_line = start_line
|
||||||
|
self.brace_count = 0
|
||||||
|
|
||||||
|
globals_set: Set[str] = set()
|
||||||
|
procs: Dict[str, Set[str]] = {}
|
||||||
|
proc_ranges: List[ProcRange] = []
|
||||||
|
|
||||||
|
scope_stack: List[Scope] = [Scope("", "root", 0)]
|
||||||
|
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
ns_match = NS_RE.match(line)
|
||||||
|
proc_match = PROC_RE.match(line)
|
||||||
|
set_match = SET_RE.match(line)
|
||||||
|
|
||||||
|
# Namespace scope
|
||||||
|
if ns_match:
|
||||||
|
scope_stack.append(Scope(ns_match.group(1), "namespace", i))
|
||||||
|
|
||||||
|
# Proc scope
|
||||||
|
elif proc_match:
|
||||||
|
pname = proc_match.group(1)
|
||||||
|
scope_stack.append(Scope(pname, "proc", i))
|
||||||
|
proc_ranges.append(ProcRange(name=pname, start_line=i, end_line=None))
|
||||||
|
|
||||||
|
# Track set statements
|
||||||
|
if set_match:
|
||||||
|
raw_name = set_match.group(1)
|
||||||
|
# Normalize array names and leading ::
|
||||||
|
base = raw_name.split("(", 1)[0]
|
||||||
|
if base.startswith("::"):
|
||||||
|
clean = base[2:]
|
||||||
|
globals_set.add(clean)
|
||||||
|
else:
|
||||||
|
top = scope_stack[-1]
|
||||||
|
if top.kind == "root":
|
||||||
|
globals_set.add(base)
|
||||||
|
elif top.kind == "proc":
|
||||||
|
procs.setdefault(top.name, set()).add(base)
|
||||||
|
else:
|
||||||
|
# inside namespace without :: -> ignore for globals
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Brace balancing for current top scope
|
||||||
|
open_count = line.count("{")
|
||||||
|
close_count = line.count("}")
|
||||||
|
scope_stack[-1].brace_count += open_count - close_count
|
||||||
|
|
||||||
|
# Close finished scopes
|
||||||
|
while len(scope_stack) > 1 and scope_stack[-1].brace_count <= 0:
|
||||||
|
finished = scope_stack.pop()
|
||||||
|
if finished.kind == "proc":
|
||||||
|
# Update the last matching proc range end_line
|
||||||
|
for pr in reversed(proc_ranges):
|
||||||
|
if pr.name == finished.name and pr.end_line is None:
|
||||||
|
pr.end_line = i
|
||||||
|
break
|
||||||
|
|
||||||
|
# Finalize any unterminated proc ranges
|
||||||
|
for pr in proc_ranges:
|
||||||
|
if pr.end_line is None:
|
||||||
|
pr.end_line = len(lines) - 1
|
||||||
|
|
||||||
|
return globals_set, procs, proc_ranges
|
||||||
|
|
||||||
Reference in New Issue
Block a user