Compare commits

..
Author SHA1 Message Date
Christoph 91c8c4aff3 Merge pull request 'Check Array Paramter' (#38) from #150 into main
build_and_puplish.yml / build_and_publish (release) Successful in 34s
2026-09-10 08:29:22 +00:00
Christoph Brandau d8611d7aea feat(tcl): add static variable name extraction and array key completion
This adds tooling to statically extract Tcl variable names from syntax
trees without evaluating substitutions, enabling better completions
for array elements and plain variables. The new helpers are wired
into the completion and navigation flows and are supported by tests
covering array keys and substitutions.

- Introduce variable_names.py with variable_name() and array_key_parts()
- Wire static name extraction into completion and symbol indexing
- Add tests for array key completion with substitutions
2026-09-10 09:07:18 +02:00
Christoph Brandau 7e9a4359cd feat(cdl): include args in generated event handler snippet
The generated event handler snippet now declares the proc with an args block.
This enables parameters to be referenced within the handler body.
The change makes the snippet compatible with events that pass arguments.

- Include an args block in the proc declaration for event handlers.
2026-09-10 08:45:25 +02:00
Christoph b7f28ab1a3 Update version to 2026.9.220 2026-09-08 19:22:31 +00:00
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
12 changed files with 365 additions and 11 deletions
+26
View File
@@ -17,6 +17,19 @@
], ],
"cwd": "${env:TEMP}/nx-post-support-vscode-debug", "cwd": "${env:TEMP}/nx-post-support-vscode-debug",
"outFiles": ["${env:TEMP}/nx-post-support-vscode-debug/dist/**/*.js"], "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, "sourceMaps": true,
"resolveSourceMapLocations": [ "resolveSourceMapLocations": [
"${env:TEMP}/nx-post-support-vscode-debug/dist/**/*.js", "${env:TEMP}/nx-post-support-vscode-debug/dist/**/*.js",
@@ -50,6 +63,19 @@
], ],
"cwd": "${env:TEMP}/nx-post-support-vscode-debug", "cwd": "${env:TEMP}/nx-post-support-vscode-debug",
"outFiles": ["${env:TEMP}/nx-post-support-vscode-debug/dist/**/*.js"], "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, "sourceMaps": true,
"resolveSourceMapLocations": [ "resolveSourceMapLocations": [
"${env:TEMP}/nx-post-support-vscode-debug/dist/**/*.js", "${env:TEMP}/nx-post-support-vscode-debug/dist/**/*.js",
+7
View File
@@ -15,6 +15,13 @@
"${workspaceFolder}", "${workspaceFolder}",
"${env:TEMP}\\nx-post-support-vscode-debug" "${env:TEMP}\\nx-post-support-vscode-debug"
], ],
"linux": {
"command": "npm",
"args": ["run", "compile:debug"],
"options": {
"cwd": "${workspaceFolder}"
}
},
"problemMatcher": [], "problemMatcher": [],
"presentation": { "presentation": {
"reveal": "always", "reveal": "always",
+1 -1
View File
@@ -105,7 +105,7 @@ export function createCdlEventHandlerSnippet(handler: CdlEventHandler): string {
const globals = [ const globals = [
...new Set(handler.parameterNames.map((parameter) => momVariableName(parameter))) ...new Set(handler.parameterNames.map((parameter) => momVariableName(parameter)))
] ]
const lines = [`proc ${momEventName(handler.eventName)} { } {`] const lines = [`proc ${momEventName(handler.eventName)} {args} {`]
if (globals.length > 0) { if (globals.length > 0) {
lines.push(...globals.map((variable) => ` global ${variable}`), "") lines.push(...globals.map((variable) => ` global ${variable}`), "")
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "nx-post-support", "name": "nx-post-support",
"displayName": "NX Postprocessor 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", "description": "VS Code extension for NX CAM postprocessor development with language support and remote Tcl debugging for CDL, TCL, and DEF files",
"version": "2026.9.200", "version": "2026.9.220",
"publisher": "Christoph", "publisher": "Christoph",
"icon": "images/nx-1.png", "icon": "images/nx-1.png",
"activationEvents": [ "activationEvents": [
+9 -2
View File
@@ -47,6 +47,7 @@ from common.load_data import standard_items
from lsp_tclserver import TclLanguageServer from lsp_tclserver import TclLanguageServer
from tools.completion_items import ( from tools.completion_items import (
CompletionContext, CompletionContext,
array_element_completions,
completion_context, completion_context,
ranked_completion_items, ranked_completion_items,
) )
@@ -271,12 +272,18 @@ def document_diagnostic(params: lsp.DocumentDiagnosticParams):
@LSP_SERVER.feature( @LSP_SERVER.feature(
lsp.TEXT_DOCUMENT_COMPLETION, lsp.TEXT_DOCUMENT_COMPLETION,
lsp.CompletionOptions(trigger_characters=["$", " ", "-"]), lsp.CompletionOptions(trigger_characters=["$", " ", "-", "(", ","]),
) )
def on_completion(params: lsp.CompletionParams) -> lsp.CompletionList: def on_completion(params: lsp.CompletionParams) -> lsp.CompletionList:
doc = LSP_SERVER.workspace.get_text_document(params.text_document.uri) doc = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
position = params.position position = params.position
source_lines = LSP_SERVER.get_lines(doc) source_lines = LSP_SERVER.get_lines(doc)
array_items = array_element_completions(
source_lines, position, LSP_SERVER.navigation_snapshot().values(),
str(pathlib.Path(uris.to_fs_path(doc.uri))),
)
if array_items is not None:
return lsp.CompletionList(is_incomplete=False, items=array_items)
context = completion_context(source_lines, position) context = completion_context(source_lines, position)
# Variable completion wins inside command arguments. Otherwise prefer the # Variable completion wins inside command arguments. Otherwise prefer the
@@ -323,7 +330,7 @@ def on_completion(params: lsp.CompletionParams) -> lsp.CompletionList:
and params.context is not None and params.context is not None
and params.context.trigger_kind and params.context.trigger_kind
== lsp.CompletionTriggerKind.TriggerCharacter == lsp.CompletionTriggerKind.TriggerCharacter
and params.context.trigger_character in {" ", "-"} and params.context.trigger_character in {" ", "-", "(", ","}
): ):
return lsp.CompletionList(is_incomplete=False, items=[]) return lsp.CompletionList(is_incomplete=False, items=[])
+97 -3
View File
@@ -9,7 +9,13 @@ import lsprotocol.types as lsp
from tclint.syntax_tree import BareWord, Command, List, Visitor from tclint.syntax_tree import BareWord, Command, List, Visitor
from common.load_data import standard_items from common.load_data import standard_items
from tools.tcl_command_completion import line_prefix_at_position from tools.navigation import FileSymbolIndex
from tools.variable_names import variable_name
from tools.tcl_command_completion import (
DynamicCompletionKind,
line_prefix_at_position,
tcl_argument_completion,
)
BUILTIN_VAR_LABELS = {ci.label for ci in standard_items.nx_variables} BUILTIN_VAR_LABELS = {ci.label for ci in standard_items.nx_variables}
BUILTIN_PROC_LABELS = {ci.label for ci in standard_items.nx_procs} BUILTIN_PROC_LABELS = {ci.label for ci in standard_items.nx_procs}
@@ -36,6 +42,94 @@ COMMAND_KINDS = {
_VARIABLE_PREFIX_RE = re.compile(r"(?<!\\)\$(?:\{)?[A-Za-z0-9_:]*$") _VARIABLE_PREFIX_RE = re.compile(r"(?<!\\)\$(?:\{)?[A-Za-z0-9_:]*$")
_COMMAND_PREFIX_RE = re.compile(r"(?:^|[;\[\{])\s*[^\s;\[\]\{\}]*$") _COMMAND_PREFIX_RE = re.compile(r"(?:^|[;\[\{])\s*[^\s;\[\]\{\}]*$")
_ARRAY_PREFIX_RE = re.compile(
r"(?P<name>(?:::)?[A-Za-z_][A-Za-z0-9_:]*)\((?P<key>[^()\n]*)$"
)
def array_element_completions(
source_lines: Sequence[str],
position: lsp.Position,
indexes: Iterable[FileSymbolIndex],
current_path: str,
) -> list[lsp.CompletionItem] | None:
"""Complete literal array keys, replacing only the text inside parentheses."""
prefix = line_prefix_at_position(source_lines, position)
if prefix is None:
return None
match = _ARRAY_PREFIX_RE.search(prefix)
if match is None:
return None
before = prefix[:match.start()]
if not before.endswith("$"):
argument = tcl_argument_completion(source_lines, position)
if argument is None or argument.dynamic_kind != DynamicCompletionKind.VARIABLE:
return None
name, key_prefix = match.group("name", "key")
part_index = key_prefix.count(",")
key_prefix = key_prefix.rsplit(",", 1)[-1]
if any(char in key_prefix for char in "$[]{}\\"):
return None
indexes = list(indexes)
local_scope = None
for index in indexes:
if index.path != current_path:
continue
for occurrence in index.occurrences:
span = occurrence.declaration_range
if (
occurrence.identity.kind == "proc"
and occurrence.is_definition
and span is not None
and span.start.line <= position.line <= span.end.line
):
local_scope = f"{index.path}::proc::{occurrence.identity.name}"
keys: dict[str, set[tuple[str | None, ...]]] = {}
for index in indexes:
for occurrence in index.occurrences:
parts = occurrence.array_parts
key = parts[part_index] if part_index < len(parts) else None
if (
key
and (
occurrence.identity.scope is None
or occurrence.identity.scope == local_scope
)
and key.startswith(key_prefix)
and not any(char in key for char in "$[]\\")
and occurrence.identity.name.removeprefix("::") == name.removeprefix("::")
):
keys.setdefault(key, set()).add(occurrence.array_template_parts[part_index + 1:])
line = source_lines[position.line]
suffix = line[len(prefix):]
remaining = re.match(r"[^(),\s$\[\]{}]*", suffix).group()
has_close = suffix[len(remaining):].startswith((")", ","))
start = position.character - len(key_prefix.encode("utf-16-le")) // 2
end = position.character + len(remaining.encode("utf-16-le")) // 2
items = []
for key in sorted(keys):
new_text = key
# Fill missing index components only; keep an existing comma and suffix.
if not suffix[len(remaining):].startswith(","):
tails = sorted(keys[key], key=lambda tail: (len(tail), repr(tail)))
tail = tails[0]
if all(part is not None for part in tail) and any("$" in part for part in tail):
new_text = ",".join([key, *tail])
new_text += "" if has_close else ")"
items.append(lsp.CompletionItem(
label=key,
kind=lsp.CompletionItemKind.Field,
detail=f"{name}({key})",
insert_text_format=lsp.InsertTextFormat.PlainText,
text_edit=lsp.TextEdit(
range=lsp.Range(
start=lsp.Position(line=position.line, character=start),
end=lsp.Position(line=position.line, character=end),
),
new_text=new_text,
),
))
return items
def completion_context( def completion_context(
@@ -167,8 +261,8 @@ class CompletionCollector(Visitor):
# Collect variables set with explicit global namespace: set ::var_name ... # Collect variables set with explicit global namespace: set ::var_name ...
elif routine.contents == "set" and command.args: elif routine.contents == "set" and command.args:
first = command.args[0] first = command.args[0]
if isinstance(first, BareWord) and getattr(first, "value", None): var_name = variable_name(first)
var_name = first.value if var_name:
if var_name.startswith("::"): if var_name.startswith("::"):
base_name = var_name.split("(", 1)[0] base_name = var_name.split("(", 1)[0]
clean_name = base_name[2:] # remove leading '::' for completion display clean_name = base_name[2:] # remove leading '::' for completion display
+15 -2
View File
@@ -4,7 +4,8 @@ from dataclasses import dataclass
from pathlib import Path from pathlib import Path
import lsprotocol.types as lsp import lsprotocol.types as lsp
from tclint.syntax_tree import Command, List, Node, Script, VarSub from tclint.syntax_tree import Command, List, Node, QuotedWord, Script, VarSub
from tools.variable_names import array_key_parts, variable_name
ROOT_NAMESPACE = "::" ROOT_NAMESPACE = "::"
@@ -27,6 +28,9 @@ class SymbolOccurrence:
fallback_identity: SymbolIdentity | None = None fallback_identity: SymbolIdentity | None = None
caller: SymbolIdentity | None = None caller: SymbolIdentity | None = None
declaration_range: lsp.Range | None = None declaration_range: lsp.Range | None = None
array_element: str | None = None
array_parts: tuple[str | None, ...] = ()
array_template_parts: tuple[str | None, ...] = ()
@dataclass(frozen=True) @dataclass(frozen=True)
@@ -96,6 +100,8 @@ def _name_range(node: Node, raw_name: str, *, variable_sub: bool = False) -> lsp
column += 2 if getattr(node, "braced", False) else 1 column += 2 if getattr(node, "braced", False) else 1
else: else:
position = getattr(node, "contents_pos", None) or node.pos position = getattr(node, "contents_pos", None) or node.pos
if isinstance(node, QuotedWord) and node.contents is None and node.children:
position = node.children[0].pos
line, column = position line, column = position
normalized = _without_array_index(raw_name) normalized = _without_array_index(raw_name)
@@ -312,6 +318,13 @@ def build_file_symbol_index(
is_definition=is_definition, is_definition=is_definition,
symbol_kind=lsp.SymbolKind.Variable, symbol_kind=lsp.SymbolKind.Variable,
container_name=_container_name(symbol_identity), container_name=_container_name(symbol_identity),
array_element=(
raw_name.split("(", 1)[1][:-1]
if "(" in raw_name and raw_name.endswith(")")
else None
),
array_parts=array_key_parts(node),
array_template_parts=array_key_parts(node, preserve_variables=True),
) )
) )
@@ -444,7 +457,7 @@ def build_file_symbol_index(
for node, is_definition in _variable_command_nodes(command): for node, is_definition in _variable_command_nodes(command):
if id(node) in declaration_ids: if id(node) in declaration_ids:
continue continue
raw_name = _static_contents(node) raw_name = variable_name(node)
if raw_name: if raw_name:
add_variable( add_variable(
node, node,
+6
View File
@@ -5,6 +5,7 @@ import attrs
from common.load_data import standard_items from common.load_data import standard_items
from tclint.commands.plugins import PluginManager from tclint.commands.plugins import PluginManager
from tclint.syntax_tree import BareWord, Command, QuotedWord, Visitor from tclint.syntax_tree import BareWord, Command, QuotedWord, Visitor
from tools.variable_names import variable_name
# Constructing a PluginManager scans entry points, and get_commands() rebuilds # Constructing a PluginManager scans entry points, and get_commands() rebuilds
# the builtin command set on every call. Semantic tokens are requested often, so # the builtin command set on every call. Semantic tokens are requested often, so
@@ -159,6 +160,11 @@ class _Highlighter(Visitor):
if routine.contents == "set" and command.args: if routine.contents == "set" and command.args:
first_arg = command.args[0] first_arg = command.args[0]
token_info = self._get_token_info(first_arg) token_info = self._get_token_info(first_arg)
if first_arg.contents is None:
name = variable_name(first_arg)
if name:
line, col = first_arg.children[0].pos
token_info = ((line - 1, col - 1), len(name))
if token_info: if token_info:
(line, col), length = token_info (line, col), length = token_info
self._append_token((line, col), length, "variable", [TokenModifier.declaration]) self._append_token((line, col), length, "variable", [TokenModifier.declaration])
+7 -1
View File
@@ -1009,7 +1009,13 @@ def _current_command_segment(line_prefix: str) -> str:
starts.append(brace_starts[-1] + 1) starts.append(brace_starts[-1] + 1)
if bracket_starts: if bracket_starts:
starts.append(bracket_starts[-1] + 1) starts.append(bracket_starts[-1] + 1)
return line_prefix[max(starts) :] start = max(starts)
if brace_starts and start == brace_starts[-1] + 1:
# An unfinished braced body or expression can itself contain commands
# (for example, ``if {[string compare ...``). Scan that inner context
# independently, while keeping completed braced arguments opaque.
return _current_command_segment(line_prefix[start:])
return line_prefix[start:]
def _tokenize_command_segment(segment: str) -> list[str]: def _tokenize_command_segment(segment: str) -> list[str]:
+2 -1
View File
@@ -4,6 +4,7 @@ from dataclasses import dataclass
from tclint.syntax_tree import Command, Node, Script from tclint.syntax_tree import Command, Node, Script
from tclint.syntax_tree import List as TclList from tclint.syntax_tree import List as TclList
from tools.variable_names import variable_name
@dataclass @dataclass
@@ -81,7 +82,7 @@ def build_variable_index(
return return
if routine == "set" and node.args: if routine == "set" and node.args:
raw_name = getattr(node.args[0], "contents", None) raw_name = variable_name(node.args[0])
base = _normalize_var_name(raw_name) base = _normalize_var_name(raw_name)
if base is not None: if base is not None:
if raw_name and raw_name.startswith("::"): if raw_name and raw_name.startswith("::"):
+44
View File
@@ -0,0 +1,44 @@
"""Extract statically known variable names without evaluating Tcl substitutions."""
from tclint.syntax_tree import BareWord, CompoundBareWord, Node, QuotedWord, VarSub
def variable_name(node: Node) -> str | None:
contents = node.contents
if isinstance(contents, str):
return contents
if isinstance(node, (CompoundBareWord, QuotedWord)) and node.children:
first = node.children[0]
last = node.children[-1]
if (
isinstance(first, BareWord)
and isinstance(last, BareWord)
and "(" in first.value
and last.value.endswith(")")
):
# Substitutions in an array index do not change the array's name.
return first.value.split("(", 1)[0] or None
return None
def array_key_parts(node: Node, *, preserve_variables: bool = False) -> tuple[str | None, ...]:
"""Keep comma-separated literal index components; substitutions are unknown."""
contents = node.contents
if not isinstance(contents, str):
if not isinstance(node, (CompoundBareWord, QuotedWord)) or not variable_name(node):
return ()
chunks = []
for child in node.children:
if isinstance(child, BareWord):
chunks.append(child.value)
elif preserve_variables and isinstance(child, VarSub) and not child.children:
chunks.append("${" + child.value + "}" if child.braced else "$" + child.value)
else:
chunks.append("\0")
contents = "".join(chunks)
if "(" not in contents or not contents.endswith(")"):
return ()
return tuple(
part if part and not any(char in part for char in ("\0[]\\" if preserve_variables else "\0$[]\\")) else None
for part in contents.split("(", 1)[1][:-1].split(",")
)
@@ -103,6 +103,133 @@ def _argument_completion_labels(source: str) -> set[str] | None:
return {item.label for item in completion.items} return {item.label for item in completion.items}
def test_array_keys_complete_in_set_and_substitution(tmp_path: Path, monkeypatch):
server, _, _ = _completion_server(tmp_path, monkeypatch)
workspace = _document(
tmp_path / "arrays.tcl",
"set ::lib_flag(enabled) 1\nset ::lib_flag(external) 1\n",
)
assert server.update_poco_completion_for_file(workspace)
source = (
"set lib_flag(enabled) 0\n"
"set lib_flag(empty) 1\n"
"set other(wrong) 1\n"
"proc hidden {} { set lib_flag(private) 1 }\n"
"set lib_flag()\n"
"puts $lib_flag(en)\n"
"puts 😀; set lib_flag(em\n"
)
current = _document(tmp_path / "arrays-current.tcl", source)
server.workspace.put_text_document(lsp.TextDocumentItem(
uri=current.uri, language_id="tcl", version=1, text=source,
))
assert server.update_poco_completion_for_file(current)
items = _complete(current, _position_after(source, "set lib_flag(", 3))
assert [item.label for item in items] == ["empty", "enabled", "external"]
assert all(item.text_edit.new_text == item.label for item in items)
items = _complete(current, _position_after(source, "puts $lib_flag(en"))
assert [item.label for item in items] == ["enabled"]
assert items[0].text_edit.new_text == "enabled"
position = lsp.Position(line=6, character=len(source.splitlines()[6].encode("utf-16-le")) // 2)
items = _complete(current, position)
assert [item.label for item in items] == ["empty"]
assert items[0].text_edit.new_text == "empty)"
assert items[0].text_edit.range.start.character == position.character - 2
def test_dynamic_array_index_keeps_variable_identity(tmp_path: Path, monkeypatch):
from tools.navigation import build_file_symbol_index
from tools.parser import CustomParser
from tools.semantic_tokens import _Highlighter
from tools.variable_index import build_variable_index
from tools.variable_names import variable_name
source = (
"set custom_flag(from_move,$::mom_path_name) 1\n"
'set "::quoted_flag(from_move,$::mom_path_name)" 1\n'
"set ::command_flag([info hostname]) 1\n"
"set ${dynamic_name}(entry) 1\n"
"proc example {} { set local_flag($::mom_path_name) 1 }\n"
"puts $custom_flag\n"
)
tree = CustomParser().parse(source)
globals_, locals_, _ = build_variable_index(source, tree)
assert {"custom_flag", "quoted_flag", "command_flag"} <= globals_
assert locals_["example"] == {"local_flag"}
assert variable_name(tree.children[3].args[0]) is None
path = tmp_path / "dynamic.tcl"
index = build_file_symbol_index(str(path), path.as_uri(), tree)
definition = next(
item for item in index.occurrences
if item.identity.name == "::custom_flag" and item.is_definition
)
assert definition.range.start.character == 4
assert definition.range.end.character == 15
assert definition.array_element is None
assert any(item.identity.name == "::mom_path_name" for item in index.occurrences)
highlighter = _Highlighter([], {})
tree.accept(highlighter, recurse=True)
assert any(
position == (0, 4) and length == 11 and kind == "variable"
for position, length, kind, _ in highlighter._tokens
)
server, _, _ = _completion_server(tmp_path, monkeypatch)
current = _document(path, source)
server.workspace.put_text_document(lsp.TextDocumentItem(
uri=current.uri, language_id="tcl", version=1, text=source,
))
assert server.update_poco_completion_for_file(current)
items = _complete(current, _position_after(source, "puts $custom"))
assert "custom_flag" in {item.label for item in items}
workspace_items = next(
items for item_path, items in server.completion_items_by_file_snapshot().items()
if server.paths_equal(item_path, str(path))
)
assert {"quoted_flag", "command_flag"} <= {item.label for item in workspace_items}
def test_literal_array_components_complete_around_substitutions(tmp_path: Path, monkeypatch):
from tools.completion_items import array_element_completions
server, _, _ = _completion_server(tmp_path, monkeypatch)
source = (
"set custom_flag(from_move,$::mom_path_name) 1\n"
"set custom_flag(to_move,$::mom_path_name) 1\n"
"set custom_flag($::mom_path_name,finished) 1\n"
"set custom_flag(prefix_$::mom_path_name,other) 1\n"
"set custom_flag([info hostname],command_tail) 1\n"
"set other_flag(wrong,$::mom_path_name) 1\n"
"set multi_flag(move,$first,axis,$second) 1\n"
)
document = _document(tmp_path / "components.tcl", source)
assert server.update_poco_completion_for_file(document)
cases = [
("set custom_flag(|,$::mom_path_name)", {"from_move", "to_move"}, "from_move", "set custom_flag(from_move,$::mom_path_name)"),
("set custom_flag(fr|om_old,$::mom_path_name)", {"from_move"}, "from_move", "set custom_flag(from_move,$::mom_path_name)"),
("puts $custom_flag($::mom_path_name,fi|)", {"finished"}, "finished", "puts $custom_flag($::mom_path_name,finished)"),
("set custom_flag($::mom_path_name,|)", {"finished", "other", "command_tail"}, "other", "set custom_flag($::mom_path_name,other)"),
("set custom_flag(fr|", {"from_move"}, "from_move", "set custom_flag(from_move,$::mom_path_name)"),
("set custom_flag(fr|)", {"from_move"}, "from_move", "set custom_flag(from_move,$::mom_path_name)"),
("set multi_flag(m|)", {"move"}, "move", "set multi_flag(move,$first,axis,$second)"),
]
for marked, labels, selected, expected in cases:
offset = marked.index("|")
line = marked.replace("|", "")
items = array_element_completions(
[line], lsp.Position(line=0, character=offset),
server.navigation_snapshot().values(), str(tmp_path / "caller.tcl"),
)
assert {item.label for item in items} == labels
edit = next(item.text_edit for item in items if item.label == selected)
item = next(item for item in items if item.label == selected)
assert item.insert_text_format == lsp.InsertTextFormat.PlainText
assert line[:edit.range.start.character] + edit.new_text + line[edit.range.end.character:] == expected
assert array_element_completions(
["set custom_flag(from_move,$::mom"], lsp.Position(line=0, character=31),
server.navigation_snapshot().values(), str(tmp_path / "caller.tcl"),
) is None
def _argument_completion_request(source: str): def _argument_completion_request(source: str):
lines = source.split("\n") lines = source.split("\n")
character = len(lines[-1].encode("utf-16-le")) // 2 character = len(lines[-1].encode("utf-16-le")) // 2
@@ -204,6 +331,29 @@ def test_string_completion_handles_nested_commands_and_is_values():
} }
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(): def test_dict_array_namespace_file_and_info_subcommands():
dict_items = _argument_completion_labels("dict ") dict_items = _argument_completion_labels("dict ")
assert dict_items is not None assert dict_items is not None