Compare commits
10
Commits
2026.9.200
...
2026.9.300
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91c8c4aff3 | ||
|
|
d8611d7aea | ||
|
|
7e9a4359cd | ||
|
|
b7f28ab1a3 | ||
|
|
9b368ba761 | ||
|
|
82e13cf7ce | ||
|
|
d104906508 | ||
|
|
a5ff3b55ff | ||
|
|
26225eb8ed | ||
|
|
844138b383 |
Vendored
+26
@@ -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",
|
||||||
|
|||||||
Vendored
+7
@@ -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",
|
||||||
|
|||||||
@@ -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
@@ -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.100",
|
"version": "2026.9.220",
|
||||||
"publisher": "Christoph",
|
"publisher": "Christoph",
|
||||||
"icon": "images/nx-1.png",
|
"icon": "images/nx-1.png",
|
||||||
"activationEvents": [
|
"activationEvents": [
|
||||||
|
|||||||
@@ -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=[])
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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])
|
||||||
|
|||||||
@@ -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]:
|
||||||
|
|||||||
@@ -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("::"):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -5,6 +5,9 @@
|
|||||||
{
|
{
|
||||||
"include": "#comment"
|
"include": "#comment"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"include": "#escaped-quoted-strings"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"include": "#strings"
|
"include": "#strings"
|
||||||
},
|
},
|
||||||
@@ -28,6 +31,31 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"repository": {
|
"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": {
|
"keywords": {
|
||||||
"patterns": [
|
"patterns": [
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user