Compare commits
4
Commits
2026.9.100
...
2026.9.200
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7eb72f417 | ||
|
|
af5acfc946 | ||
|
|
20f76b6a20 | ||
|
|
081b488fe3 |
@@ -3,6 +3,9 @@
|
||||
- Add incoming and outgoing call hierarchy for custom TCL procedures and MOM event handlers
|
||||
- Add document highlights for procedure and variable occurrences
|
||||
- Make completion context-aware and prioritize local, current-file, workspace, and built-in symbols
|
||||
- Add command-aware completion for Tcl subcommands, fixed arguments, and valid options
|
||||
- Add semantic argument completion for variables, procedures, namespaces, and local file paths
|
||||
- Add placeholder-based snippets for common Tcl structures and `dict for`
|
||||
- Integrate the NX Tcl Remote Debugger directly into NX Postprocessor Support
|
||||
- Add `nx-tcl` attach configurations and breakpoint support for TCL and DEF files
|
||||
- Support breakpoints, stepping, stack frames, variables, watches, evaluation, logpoints, hit conditions, and Tcl error stops
|
||||
|
||||
@@ -12,7 +12,8 @@ A comprehensive VS Code extension providing language support and remote debuggin
|
||||
- **Signature Help** - Shows parameters and documentation for custom and NX procedures
|
||||
- **Call Hierarchy** - Traces incoming and outgoing calls between custom TCL procedures and MOM event handlers
|
||||
- **Document Highlights** - Highlights all reads, writes, and calls of the symbol under the cursor
|
||||
- **Context-aware Completion** - Prioritizes local symbols and suggests variables or commands based on cursor context
|
||||
- **Context-aware Completion** - Prioritizes local symbols and suggests variables, procedures, namespaces, paths, Tcl subcommands, valid argument values, and options based on cursor context
|
||||
- **Tcl Snippets** - Inserts placeholder-based structures for `if`, `foreach`, `proc`, `switch`, `try`, and `dict for`
|
||||
- **NX Tcl Remote Debugger** - Breakpoints, stepping, call stack, scopes, variables, watches, evaluation, logpoints, hit conditions, and Tcl error stops directly in a running NX Post process
|
||||
|
||||
## Supported File Types
|
||||
@@ -101,7 +102,8 @@ Simply open any supported file type and enjoy:
|
||||
- Signature help while entering procedure arguments
|
||||
- Incoming and outgoing call hierarchy for custom procedures and MOM event handlers
|
||||
- Document-wide highlights for procedure and variable occurrences
|
||||
- Context-aware completion with local symbols ranked before workspace and built-in symbols
|
||||
- Context-aware completion with local symbols ranked before workspace and built-in symbols, plus semantic arguments, local paths, Tcl subcommands, and options such as `string compare -nocase`
|
||||
- Placeholder-based snippets for common Tcl control structures and procedures
|
||||
- Remote NX Tcl debugging with breakpoints and full stepping
|
||||
|
||||
## Contributing
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"name": "nx-post-support",
|
||||
"displayName": "NX Postprocessor Support",
|
||||
"description": "VS Code extension for NX CAM postprocessor development with language support and remote Tcl debugging for CDL, TCL, and DEF files",
|
||||
"version": "2026.8.300",
|
||||
"version": "2026.9.100",
|
||||
"publisher": "Christoph",
|
||||
"icon": "images/nx-1.png",
|
||||
"activationEvents": [
|
||||
|
||||
+143
-9
@@ -38,13 +38,18 @@ update_sys_path(
|
||||
# Imports needed for the language server goes below this.
|
||||
# **********************************************************
|
||||
# pylint: disable=wrong-import-position,import-error
|
||||
import lsp_jsonrpc as jsonrpc
|
||||
import lsprotocol.types as lsp
|
||||
from common.load_data import standard_items
|
||||
from lsp_tclserver import TclLanguageServer
|
||||
from pygls import uris
|
||||
from pygls.workspace.text_document import TextDocument
|
||||
from tools.completion_items import completion_context, ranked_completion_items
|
||||
|
||||
import lsp_jsonrpc as jsonrpc
|
||||
from common.load_data import standard_items
|
||||
from lsp_tclserver import TclLanguageServer
|
||||
from tools.completion_items import (
|
||||
CompletionContext,
|
||||
completion_context,
|
||||
ranked_completion_items,
|
||||
)
|
||||
from tools.folding_ranges import build_folding_ranges
|
||||
from tools.inlay_hint import (
|
||||
InlayHintGenerator,
|
||||
@@ -69,6 +74,13 @@ from tools.semantic_tokens import (
|
||||
_Highlighter,
|
||||
)
|
||||
from tools.signature_help import build_signature_help
|
||||
from tools.tcl_command_completion import (
|
||||
TCL_COMMAND_ITEMS,
|
||||
TCL_COMMAND_NAMES,
|
||||
DynamicCompletionKind,
|
||||
path_completion_items,
|
||||
tcl_argument_completion,
|
||||
)
|
||||
|
||||
WORKSPACE_SETTINGS = {}
|
||||
GLOBAL_SETTINGS = {}
|
||||
@@ -82,10 +94,19 @@ LSP_SERVER = TclLanguageServer(
|
||||
BUILTIN_PROC_NAMES = {
|
||||
item.label
|
||||
for item in standard_items.tcl_keyword_list + standard_items.nx_procs
|
||||
}
|
||||
} | set(TCL_COMMAND_NAMES)
|
||||
BUILTIN_VARIABLE_NAMES = {item.label for item in standard_items.nx_variables}
|
||||
_TCL_COMMAND_ITEMS_BY_LABEL = {
|
||||
item.label: item for item in TCL_COMMAND_ITEMS
|
||||
}
|
||||
_TCL_KEYWORD_ITEMS = [
|
||||
_TCL_COMMAND_ITEMS_BY_LABEL.get(item.label, item)
|
||||
for item in standard_items.tcl_keyword_list
|
||||
]
|
||||
_STATIC_TCL_LABELS = {item.label for item in standard_items.tcl_keyword_list}
|
||||
STATIC_COMPLETION_ITEMS = tuple(
|
||||
standard_items.tcl_keyword_list
|
||||
_TCL_KEYWORD_ITEMS
|
||||
+ [item for item in TCL_COMMAND_ITEMS if item.label not in _STATIC_TCL_LABELS]
|
||||
+ standard_items.nx_procs
|
||||
+ standard_items.nx_variables
|
||||
)
|
||||
@@ -250,16 +271,67 @@ def document_diagnostic(params: lsp.DocumentDiagnosticParams):
|
||||
|
||||
@LSP_SERVER.feature(
|
||||
lsp.TEXT_DOCUMENT_COMPLETION,
|
||||
lsp.CompletionOptions(trigger_characters=["$"]),
|
||||
lsp.CompletionOptions(trigger_characters=["$", " ", "-"]),
|
||||
)
|
||||
def on_completion(params: lsp.CompletionParams) -> lsp.CompletionList:
|
||||
doc = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
|
||||
position = params.position
|
||||
source_lines = LSP_SERVER.get_lines(doc)
|
||||
context = completion_context(source_lines, position)
|
||||
|
||||
# Variable completion wins inside command arguments. Otherwise prefer the
|
||||
# narrow command grammar when the cursor is at a known subcommand/option.
|
||||
argument_completion = None
|
||||
if context != CompletionContext.VARIABLE:
|
||||
argument_completion = tcl_argument_completion(source_lines, position)
|
||||
if (
|
||||
argument_completion is not None
|
||||
and argument_completion.dynamic_kind is None
|
||||
):
|
||||
items = ranked_completion_items(
|
||||
((0, item) for item in argument_completion.items),
|
||||
CompletionContext.GENERAL,
|
||||
)
|
||||
return lsp.CompletionList(is_incomplete=False, items=items)
|
||||
|
||||
if (
|
||||
argument_completion is not None
|
||||
and argument_completion.dynamic_kind == DynamicCompletionKind.PATH
|
||||
):
|
||||
dynamic_items: tuple[lsp.CompletionItem, ...] = ()
|
||||
if doc.uri.startswith("file:"):
|
||||
document_path = pathlib.Path(uris.to_fs_path(doc.uri))
|
||||
dynamic_items = path_completion_items(
|
||||
document_path.parent,
|
||||
argument_completion,
|
||||
position,
|
||||
)
|
||||
path_candidates = [
|
||||
(0, item) for item in argument_completion.items
|
||||
]
|
||||
path_candidates.extend((10, item) for item in dynamic_items)
|
||||
items = ranked_completion_items(
|
||||
path_candidates,
|
||||
CompletionContext.GENERAL,
|
||||
)
|
||||
return lsp.CompletionList(is_incomplete=False, items=items)
|
||||
|
||||
# Space and dash are registered only to open command-aware suggestions.
|
||||
# Do not display the broad fallback list when such a trigger has no match.
|
||||
if (
|
||||
argument_completion is None
|
||||
and params.context is not None
|
||||
and params.context.trigger_kind
|
||||
== lsp.CompletionTriggerKind.TriggerCharacter
|
||||
and params.context.trigger_character in {" ", "-"}
|
||||
):
|
||||
return lsp.CompletionList(is_incomplete=False, items=[])
|
||||
|
||||
tree = LSP_SERVER.get_tree(doc)
|
||||
globals_set, procs_locals, proc_ranges = LSP_SERVER.variable_index_for_document(
|
||||
doc, tree
|
||||
)
|
||||
|
||||
position = params.position
|
||||
local_names: set[str] = set()
|
||||
for proc_range in proc_ranges:
|
||||
end_line = proc_range.end_line or proc_range.start_line
|
||||
@@ -301,8 +373,70 @@ def on_completion(params: lsp.CompletionParams) -> lsp.CompletionList:
|
||||
priority = 100 if LSP_SERVER.paths_equal(item_path, filepath) else 200
|
||||
candidates.extend((priority, item) for item in items_by_file[item_path])
|
||||
|
||||
if argument_completion is not None:
|
||||
static_candidates = [(0, item) for item in argument_completion.items]
|
||||
if argument_completion.dynamic_kind == DynamicCompletionKind.VARIABLE:
|
||||
variable_candidates = [*static_candidates, *candidates]
|
||||
variable_candidates.extend(
|
||||
(300, item) for item in standard_items.nx_variables
|
||||
)
|
||||
items = ranked_completion_items(
|
||||
variable_candidates,
|
||||
CompletionContext.VARIABLE,
|
||||
)
|
||||
return lsp.CompletionList(is_incomplete=False, items=items)
|
||||
|
||||
if argument_completion.dynamic_kind == DynamicCompletionKind.PROCEDURE:
|
||||
procedure_kinds = {
|
||||
lsp.CompletionItemKind.Constructor,
|
||||
lsp.CompletionItemKind.Function,
|
||||
lsp.CompletionItemKind.Method,
|
||||
}
|
||||
procedure_candidates = [
|
||||
(priority, item)
|
||||
for priority, item in candidates
|
||||
if item.kind in procedure_kinds
|
||||
]
|
||||
procedure_candidates.extend(
|
||||
(300, item) for item in standard_items.nx_procs
|
||||
)
|
||||
items = ranked_completion_items(
|
||||
[*static_candidates, *procedure_candidates],
|
||||
CompletionContext.GENERAL,
|
||||
)
|
||||
return lsp.CompletionList(is_incomplete=False, items=items)
|
||||
|
||||
if argument_completion.dynamic_kind == DynamicCompletionKind.NAMESPACE:
|
||||
namespace_candidates = list(static_candidates)
|
||||
prefix_is_absolute = argument_completion.active_prefix.startswith("::")
|
||||
for index in LSP_SERVER.navigation_snapshot().values():
|
||||
priority = (
|
||||
100
|
||||
if LSP_SERVER.paths_equal(index.path, filepath)
|
||||
else 200
|
||||
)
|
||||
for occurrence in index.occurrences:
|
||||
if occurrence.identity.kind != "namespace":
|
||||
continue
|
||||
name = occurrence.identity.name
|
||||
label = name if prefix_is_absolute else name.removeprefix("::")
|
||||
namespace_candidates.append(
|
||||
(
|
||||
priority,
|
||||
lsp.CompletionItem(
|
||||
label=label,
|
||||
kind=lsp.CompletionItemKind.Module,
|
||||
detail="Tcl namespace",
|
||||
),
|
||||
)
|
||||
)
|
||||
items = ranked_completion_items(
|
||||
namespace_candidates,
|
||||
CompletionContext.GENERAL,
|
||||
)
|
||||
return lsp.CompletionList(is_incomplete=False, items=items)
|
||||
|
||||
candidates.extend((300, item) for item in STATIC_COMPLETION_ITEMS)
|
||||
context = completion_context(LSP_SERVER.get_lines(doc), position)
|
||||
items = ranked_completion_items(candidates, context)
|
||||
return lsp.CompletionList(is_incomplete=False, items=items)
|
||||
|
||||
|
||||
@@ -6,9 +6,11 @@ from collections.abc import Iterable, Sequence
|
||||
from enum import Enum
|
||||
|
||||
import lsprotocol.types as lsp
|
||||
from common.load_data import standard_items
|
||||
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}
|
||||
|
||||
@@ -29,33 +31,19 @@ COMMAND_KINDS = {
|
||||
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 _codepoint_offset(line: str, utf16_offset: int) -> int:
|
||||
"""Translate an LSP UTF-16 character offset into a Python string offset."""
|
||||
if utf16_offset <= 0:
|
||||
return 0
|
||||
|
||||
units = 0
|
||||
for offset, character in enumerate(line):
|
||||
units += 2 if ord(character) > 0xFFFF else 1
|
||||
if units >= utf16_offset:
|
||||
return offset + 1
|
||||
return len(line)
|
||||
|
||||
|
||||
def completion_context(
|
||||
source_lines: Sequence[str], position: lsp.Position
|
||||
) -> CompletionContext:
|
||||
if position.line < 0 or position.line >= len(source_lines):
|
||||
prefix = line_prefix_at_position(source_lines, position)
|
||||
if prefix is None:
|
||||
return CompletionContext.GENERAL
|
||||
|
||||
line = source_lines[position.line]
|
||||
prefix = line[: _codepoint_offset(line, position.character)]
|
||||
if _VARIABLE_PREFIX_RE.search(prefix):
|
||||
return CompletionContext.VARIABLE
|
||||
if _COMMAND_PREFIX_RE.search(prefix):
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, List, Set
|
||||
|
||||
from tclint.syntax_tree import Command, Node, Script
|
||||
from tclint.syntax_tree import List as TclList
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -18,13 +18,14 @@ def _normalize_var_name(raw_name: str | None) -> str | None:
|
||||
return None
|
||||
|
||||
base = raw_name.split("(", 1)[0]
|
||||
if base.startswith("::"):
|
||||
base = base[2:]
|
||||
base = base.removeprefix("::")
|
||||
|
||||
return base or None
|
||||
|
||||
|
||||
def build_variable_index(source: str, tree: Node | None = None) -> tuple[Set[str], Dict[str, Set[str]], List[ProcRange]]:
|
||||
def build_variable_index(
|
||||
source: str, tree: Node | None = None
|
||||
) -> tuple[set[str], dict[str, set[str]], list[ProcRange]]:
|
||||
"""
|
||||
Parse Tcl source text and build:
|
||||
- globals: set of variable names considered global suggestions
|
||||
@@ -40,9 +41,9 @@ def build_variable_index(source: str, tree: Node | None = None) -> tuple[Set[str
|
||||
"""
|
||||
_ = source # Kept for signature compatibility with callers.
|
||||
|
||||
globals_set: Set[str] = set()
|
||||
procs: Dict[str, Set[str]] = {}
|
||||
proc_ranges: List[ProcRange] = []
|
||||
globals_set: set[str] = set()
|
||||
procs: dict[str, set[str]] = {}
|
||||
proc_ranges: list[ProcRange] = []
|
||||
|
||||
if tree is None:
|
||||
return globals_set, procs, proc_ranges
|
||||
@@ -54,7 +55,16 @@ def build_variable_index(source: str, tree: Node | None = None) -> tuple[Set[str
|
||||
if routine == "proc" and len(node.args) >= 3 and isinstance(node.args[2], Script):
|
||||
proc_name = _normalize_var_name(getattr(node.args[0], "contents", None))
|
||||
if proc_name is not None:
|
||||
procs.setdefault(proc_name, set())
|
||||
local_variables = procs.setdefault(proc_name, set())
|
||||
for parameter in getattr(node.args[1], "children", []):
|
||||
parameter_node = parameter
|
||||
if isinstance(parameter, TclList) and parameter.children:
|
||||
parameter_node = parameter.children[0]
|
||||
parameter_name = _normalize_var_name(
|
||||
getattr(parameter_node, "contents", None)
|
||||
)
|
||||
if parameter_name is not None:
|
||||
local_variables.add(parameter_name)
|
||||
proc_ranges.append(
|
||||
ProcRange(
|
||||
name=proc_name,
|
||||
|
||||
@@ -6,18 +6,24 @@ 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 (
|
||||
DynamicCompletionKind,
|
||||
path_completion_items,
|
||||
tcl_argument_completion,
|
||||
)
|
||||
|
||||
|
||||
def _position_after(source: str, token: str, occurrence: int = 0) -> lsp.Position:
|
||||
@@ -90,6 +96,22 @@ def _complete(document: TextDocument, position: lsp.Position):
|
||||
).items
|
||||
|
||||
|
||||
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
|
||||
return tcl_argument_completion(
|
||||
lines,
|
||||
lsp.Position(line=len(lines) - 1, character=character),
|
||||
)
|
||||
|
||||
|
||||
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 +172,316 @@ 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 == []
|
||||
|
||||
|
||||
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)
|
||||
|
||||
@@ -117,7 +117,6 @@ proc SERVICE_get_tool_data {} {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
LIB_GE_command_buffer_edit_replace MOM_end_of_program_LIB END_OF_PROGRAM @END_OF_PROG {
|
||||
MOM_do_template "end_of_program_rewind"
|
||||
} EndOfProgramRewind
|
||||
|
||||
Reference in New Issue
Block a user