Compare commits

...
10 Commits
Author SHA1 Message Date
Christoph 9b368ba761 Merge pull request 'fix(completion): scan nested commands in unfinished braced args' (#37) from #36 into main
build_and_puplish.yml / build_and_publish (release) Successful in 34s
Reviewed-on: #37
2026-09-08 19:21:49 +00:00
Christoph 82e13cf7ce fix(completion): scan nested commands in unfinished braced args
Make the completion parser inspect nested command segments when an
unfinished braced argument is the active context, instead of treating the
entire braced body as opaque. This resolves cases where inner commands
(e.g. command substitutions) were ignored and improves suggestion
accuracy. Tests were added to cover nested commands inside braced
conditions and to ensure closed braced arguments do not change context.
Also add Linux-specific launch and task configurations to IDE settings
to simplify local extension development and debugging.

- Recursively scan inner context when a braced body is unfinished
- Add tests for nested commands and closed-brace behavior
- Add Linux launch/task entries for easier development
2026-09-08 21:16:58 +02:00
Christoph d104906508 Update version to 2026.9.210 2026-09-04 11:46:55 +00:00
Christoph a5ff3b55ff Merge pull request 'feat(syntax): add escaped-quoted-strings token to def syntax' (#35) from new_completions into main
build_and_puplish.yml / build_and_publish (release) Successful in 7m35s
Reviewed-on: #35
2026-09-04 11:39:08 +00:00
Christoph Brandau 26225eb8ed feat(syntax): add escaped-quoted-strings token to def syntax
Introduces an escaped-quoted-strings pattern for the def syntax.
It enables proper highlighting of strings with escapes and vars.
The change adds the new pattern to the syntax and fixes the EOF newline.

- Adds escaped-quoted-strings for escapes and embedded vars
- Registers new pattern in repository for proper highlighting
- Ensures trailing newline in the file
2026-09-04 08:39:46 +02:00
Christoph 844138b383 Update version to 2026.9.200
build_and_puplish.yml / build_and_publish (release) Canceled after 33s
2026-09-03 08:39:22 +00:00
Christoph d7eb72f417 Merge pull request 'New completions' (#34) from new_completions into main
build_and_puplish.yml / build_and_publish (release) Successful in 27s
Reviewed-on: #34
2026-09-03 08:36:00 +00:00
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
Christoph Brandau 20f76b6a20 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.
2026-09-03 10:17:16 +02:00
Christoph 081b488fe3 Update version to 2026.9.100 2026-09-03 07:38:12 +00:00
12 changed files with 1688 additions and 43 deletions
+26
View File
@@ -17,6 +17,19 @@
],
"cwd": "${env:TEMP}/nx-post-support-vscode-debug",
"outFiles": ["${env:TEMP}/nx-post-support-vscode-debug/dist/**/*.js"],
"linux": {
"args": [
"--extensionDevelopmentPath=${workspaceFolder}",
"${workspaceFolder}",
"${workspaceFolder}/test/test.tcl"
],
"cwd": "${workspaceFolder}",
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
"resolveSourceMapLocations": [
"${workspaceFolder}/dist/**/*.js",
"!**/node_modules/**"
]
},
"sourceMaps": true,
"resolveSourceMapLocations": [
"${env:TEMP}/nx-post-support-vscode-debug/dist/**/*.js",
@@ -50,6 +63,19 @@
],
"cwd": "${env:TEMP}/nx-post-support-vscode-debug",
"outFiles": ["${env:TEMP}/nx-post-support-vscode-debug/dist/**/*.js"],
"linux": {
"args": [
"--extensionDevelopmentPath=${workspaceFolder}",
"${workspaceFolder}",
"${workspaceFolder}/test/test.tcl"
],
"cwd": "${workspaceFolder}",
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
"resolveSourceMapLocations": [
"${workspaceFolder}/dist/**/*.js",
"!**/node_modules/**"
]
},
"sourceMaps": true,
"resolveSourceMapLocations": [
"${env:TEMP}/nx-post-support-vscode-debug/dist/**/*.js",
+7
View File
@@ -15,6 +15,13 @@
"${workspaceFolder}",
"${env:TEMP}\\nx-post-support-vscode-debug"
],
"linux": {
"command": "npm",
"args": ["run", "compile:debug"],
"options": {
"cwd": "${workspaceFolder}"
}
},
"problemMatcher": [],
"presentation": {
"reveal": "always",
+3
View File
@@ -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
+4 -2
View File
@@ -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
View File
@@ -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.210",
"publisher": "Christoph",
"icon": "images/nx-1.png",
"activationEvents": [
+143 -9
View File
@@ -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 -18
View File
@@ -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
+18 -8
View File
@@ -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,339 @@ 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_string_completion_inside_braced_conditions_and_bodies():
for prefix in ("if {", "while {", "proc example {} { if {"):
subcommands = _argument_completion_labels(prefix + "[string ")
assert subcommands is not None
assert {"compare", "equal", "is"} <= subcommands
assert _argument_completion_labels(prefix + "[string compare -") == {
"-length", "-nocase"
}
assert _argument_completion_labels(
prefix + "[string compare -nocase "
) == {"-length"}
def test_closed_braced_arguments_do_not_change_completion_context():
assert _argument_completion_labels("puts {[string compare }") is None
assert _argument_completion_labels(
"if {[string equal a b]} {string compare "
) == {"-length", "-nocase"}
assert _argument_completion_labels(
"if {[string equal a b] && [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)
+29 -1
View File
@@ -5,6 +5,9 @@
{
"include": "#comment"
},
{
"include": "#escaped-quoted-strings"
},
{
"include": "#strings"
},
@@ -28,6 +31,31 @@
}
],
"repository": {
"escaped-quoted-strings": {
"name": "string.quoted.double.def",
"begin": "\\\\\"",
"end": "\\\\\"",
"beginCaptures": {
"0": {
"name": "punctuation.definition.string.begin.def"
}
},
"endCaptures": {
"0": {
"name": "punctuation.definition.string.end.def"
}
},
"patterns": [
{
"name": "constant.character.escape.def",
"match": "\\\\."
},
{
"name": "variable.other.property",
"match": "\\$[_a-zA-Z][_a-zA-Z0-9]*(\\(\\$?[_a-zA-Z][_a-zA-Z0-9]*(,\\$?[_a-zA-Z][_a-zA-Z0-9]*)*\\))?"
}
]
},
"keywords": {
"patterns": [
{
@@ -151,4 +179,4 @@
}
},
"scopeName": "source.def"
}
}
-1
View File
@@ -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