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.
This commit is contained in:
@@ -4,6 +4,8 @@
|
|||||||
- Add document highlights for procedure and variable occurrences
|
- Add document highlights for procedure and variable occurrences
|
||||||
- Make completion context-aware and prioritize local, current-file, workspace, and built-in symbols
|
- 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 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
|
- Integrate the NX Tcl Remote Debugger directly into NX Postprocessor Support
|
||||||
- Add `nx-tcl` attach configurations and breakpoint support for TCL and DEF files
|
- 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
|
- 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
|
- **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
|
- **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
|
- **Document Highlights** - Highlights all reads, writes, and calls of the symbol under the cursor
|
||||||
- **Context-aware Completion** - Prioritizes local symbols and suggests variables, commands, Tcl subcommands, and valid options 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
|
- **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
|
## Supported File Types
|
||||||
@@ -101,7 +102,8 @@ Simply open any supported file type and enjoy:
|
|||||||
- Signature help while entering procedure arguments
|
- Signature help while entering procedure arguments
|
||||||
- Incoming and outgoing call hierarchy for custom procedures and MOM event handlers
|
- Incoming and outgoing call hierarchy for custom procedures and MOM event handlers
|
||||||
- Document-wide highlights for procedure and variable occurrences
|
- Document-wide highlights for procedure and variable occurrences
|
||||||
- Context-aware completion with local symbols ranked before workspace and built-in symbols, plus Tcl subcommands and options such as `string compare -nocase`
|
- 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
|
- Remote NX Tcl debugging with breakpoints and full stepping
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|||||||
+105
-6
@@ -77,7 +77,9 @@ from tools.signature_help import build_signature_help
|
|||||||
from tools.tcl_command_completion import (
|
from tools.tcl_command_completion import (
|
||||||
TCL_COMMAND_ITEMS,
|
TCL_COMMAND_ITEMS,
|
||||||
TCL_COMMAND_NAMES,
|
TCL_COMMAND_NAMES,
|
||||||
tcl_argument_completions,
|
DynamicCompletionKind,
|
||||||
|
path_completion_items,
|
||||||
|
tcl_argument_completion,
|
||||||
)
|
)
|
||||||
|
|
||||||
WORKSPACE_SETTINGS = {}
|
WORKSPACE_SETTINGS = {}
|
||||||
@@ -94,9 +96,16 @@ BUILTIN_PROC_NAMES = {
|
|||||||
for item in standard_items.tcl_keyword_list + standard_items.nx_procs
|
for item in standard_items.tcl_keyword_list + standard_items.nx_procs
|
||||||
} | set(TCL_COMMAND_NAMES)
|
} | set(TCL_COMMAND_NAMES)
|
||||||
BUILTIN_VARIABLE_NAMES = {item.label for item in standard_items.nx_variables}
|
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_TCL_LABELS = {item.label for item in standard_items.tcl_keyword_list}
|
||||||
STATIC_COMPLETION_ITEMS = tuple(
|
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]
|
+ [item for item in TCL_COMMAND_ITEMS if item.label not in _STATIC_TCL_LABELS]
|
||||||
+ standard_items.nx_procs
|
+ standard_items.nx_procs
|
||||||
+ standard_items.nx_variables
|
+ standard_items.nx_variables
|
||||||
@@ -272,11 +281,37 @@ def on_completion(params: lsp.CompletionParams) -> lsp.CompletionList:
|
|||||||
|
|
||||||
# Variable completion wins inside command arguments. Otherwise prefer the
|
# Variable completion wins inside command arguments. Otherwise prefer the
|
||||||
# narrow command grammar when the cursor is at a known subcommand/option.
|
# narrow command grammar when the cursor is at a known subcommand/option.
|
||||||
|
argument_completion = None
|
||||||
if context != CompletionContext.VARIABLE:
|
if context != CompletionContext.VARIABLE:
|
||||||
argument_items = tcl_argument_completions(source_lines, position)
|
argument_completion = tcl_argument_completion(source_lines, position)
|
||||||
if argument_items is not None:
|
if (
|
||||||
|
argument_completion is not None
|
||||||
|
and argument_completion.dynamic_kind is None
|
||||||
|
):
|
||||||
items = ranked_completion_items(
|
items = ranked_completion_items(
|
||||||
((0, item) for item in argument_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,
|
CompletionContext.GENERAL,
|
||||||
)
|
)
|
||||||
return lsp.CompletionList(is_incomplete=False, items=items)
|
return lsp.CompletionList(is_incomplete=False, items=items)
|
||||||
@@ -284,7 +319,8 @@ def on_completion(params: lsp.CompletionParams) -> lsp.CompletionList:
|
|||||||
# Space and dash are registered only to open command-aware suggestions.
|
# 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.
|
# Do not display the broad fallback list when such a trigger has no match.
|
||||||
if (
|
if (
|
||||||
params.context is not None
|
argument_completion is 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 {" ", "-"}
|
||||||
@@ -337,6 +373,69 @@ def on_completion(params: lsp.CompletionParams) -> lsp.CompletionList:
|
|||||||
priority = 100 if LSP_SERVER.paths_equal(item_path, filepath) else 200
|
priority = 100 if LSP_SERVER.paths_equal(item_path, filepath) else 200
|
||||||
candidates.extend((priority, item) for item in items_by_file[item_path])
|
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)
|
candidates.extend((300, item) for item in STATIC_COMPLETION_ITEMS)
|
||||||
items = ranked_completion_items(candidates, context)
|
items = ranked_completion_items(candidates, context)
|
||||||
return lsp.CompletionList(is_incomplete=False, items=items)
|
return lsp.CompletionList(is_incomplete=False, items=items)
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ COMMAND_KINDS = {
|
|||||||
lsp.CompletionItemKind.Method,
|
lsp.CompletionItemKind.Method,
|
||||||
lsp.CompletionItemKind.Constructor,
|
lsp.CompletionItemKind.Constructor,
|
||||||
lsp.CompletionItemKind.Keyword,
|
lsp.CompletionItemKind.Keyword,
|
||||||
|
lsp.CompletionItemKind.Snippet,
|
||||||
}
|
}
|
||||||
|
|
||||||
_VARIABLE_PREFIX_RE = re.compile(r"(?<!\\)\$(?:\{)?[A-Za-z0-9_:]*$")
|
_VARIABLE_PREFIX_RE = re.compile(r"(?<!\\)\$(?:\{)?[A-Za-z0-9_:]*$")
|
||||||
|
|||||||
@@ -4,8 +4,44 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from enum import Enum
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from lsprotocol.types import CompletionItem, CompletionItemKind, Position
|
from lsprotocol.types import (
|
||||||
|
CompletionItem,
|
||||||
|
CompletionItemKind,
|
||||||
|
InsertTextFormat,
|
||||||
|
Position,
|
||||||
|
Range,
|
||||||
|
TextEdit,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DynamicCompletionKind(Enum):
|
||||||
|
"""Workspace- or filesystem-backed completion requested by the grammar."""
|
||||||
|
|
||||||
|
VARIABLE = "variable"
|
||||||
|
PROCEDURE = "procedure"
|
||||||
|
NAMESPACE = "namespace"
|
||||||
|
PATH = "path"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class TclArgumentCompletion:
|
||||||
|
"""Static suggestions plus an optional dynamic completion category."""
|
||||||
|
|
||||||
|
items: tuple[CompletionItem, ...] = ()
|
||||||
|
dynamic_kind: DynamicCompletionKind | None = None
|
||||||
|
active_prefix: str = ""
|
||||||
|
path_extensions: tuple[str, ...] = ()
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class DynamicCompletionRule:
|
||||||
|
path: tuple[str, ...]
|
||||||
|
argument_indices: frozenset[int]
|
||||||
|
kind: DynamicCompletionKind
|
||||||
|
path_extensions: tuple[str, ...] = ()
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -343,7 +379,11 @@ OPTIONS_BY_PATH: dict[tuple[str, ...], tuple[OptionSpec, ...]] = {
|
|||||||
OptionSpec("--"),
|
OptionSpec("--"),
|
||||||
),
|
),
|
||||||
("return",): (
|
("return",): (
|
||||||
OptionSpec("-code", takes_value=True),
|
OptionSpec(
|
||||||
|
"-code",
|
||||||
|
takes_value=True,
|
||||||
|
values=("ok", "error", "return", "break", "continue"),
|
||||||
|
),
|
||||||
OptionSpec("-errorcode", takes_value=True),
|
OptionSpec("-errorcode", takes_value=True),
|
||||||
OptionSpec("-errorinfo", takes_value=True),
|
OptionSpec("-errorinfo", takes_value=True),
|
||||||
OptionSpec("-errorstack", takes_value=True),
|
OptionSpec("-errorstack", takes_value=True),
|
||||||
@@ -383,23 +423,266 @@ for _string_class in STRING_CLASSES:
|
|||||||
|
|
||||||
VALUES_BY_POSITION: dict[tuple[tuple[str, ...], int], tuple[str, ...]] = {
|
VALUES_BY_POSITION: dict[tuple[tuple[str, ...], int], tuple[str, ...]] = {
|
||||||
(("array", "names"), 3): ("-exact", "-glob", "-regexp"),
|
(("array", "names"), 3): ("-exact", "-glob", "-regexp"),
|
||||||
|
(("close",), 2): ("read", "write"),
|
||||||
|
(("open",), 2): ("r", "r+", "w", "w+", "a", "a+"),
|
||||||
|
(("package", "prefer"), 2): ("latest", "stable"),
|
||||||
|
(("seek",), 3): ("start", "current", "end"),
|
||||||
(("string", "is"), 2): STRING_CLASSES,
|
(("string", "is"), 2): STRING_CLASSES,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
_REPEATED_ARGUMENTS = frozenset(range(1, 33))
|
||||||
|
_REPEATED_SUBCOMMAND_ARGUMENTS = frozenset(range(2, 33))
|
||||||
|
|
||||||
|
DYNAMIC_COMPLETION_RULES = (
|
||||||
|
# Variable-taking commands.
|
||||||
|
DynamicCompletionRule(("append",), frozenset({1}), DynamicCompletionKind.VARIABLE),
|
||||||
|
DynamicCompletionRule(
|
||||||
|
("array", "exists"), frozenset({2}), DynamicCompletionKind.VARIABLE
|
||||||
|
),
|
||||||
|
DynamicCompletionRule(
|
||||||
|
("array", "get"), frozenset({2}), DynamicCompletionKind.VARIABLE
|
||||||
|
),
|
||||||
|
DynamicCompletionRule(
|
||||||
|
("array", "names"), frozenset({2}), DynamicCompletionKind.VARIABLE
|
||||||
|
),
|
||||||
|
DynamicCompletionRule(
|
||||||
|
("array", "set"), frozenset({2}), DynamicCompletionKind.VARIABLE
|
||||||
|
),
|
||||||
|
DynamicCompletionRule(
|
||||||
|
("array", "size"), frozenset({2}), DynamicCompletionKind.VARIABLE
|
||||||
|
),
|
||||||
|
DynamicCompletionRule(
|
||||||
|
("array", "statistics"), frozenset({2}), DynamicCompletionKind.VARIABLE
|
||||||
|
),
|
||||||
|
DynamicCompletionRule(
|
||||||
|
("array", "unset"), frozenset({2}), DynamicCompletionKind.VARIABLE
|
||||||
|
),
|
||||||
|
DynamicCompletionRule(
|
||||||
|
("catch",), frozenset({2, 3}), DynamicCompletionKind.VARIABLE
|
||||||
|
),
|
||||||
|
DynamicCompletionRule(
|
||||||
|
("dict", "append"), frozenset({2}), DynamicCompletionKind.VARIABLE
|
||||||
|
),
|
||||||
|
DynamicCompletionRule(
|
||||||
|
("dict", "incr"), frozenset({2}), DynamicCompletionKind.VARIABLE
|
||||||
|
),
|
||||||
|
DynamicCompletionRule(
|
||||||
|
("dict", "lappend"), frozenset({2}), DynamicCompletionKind.VARIABLE
|
||||||
|
),
|
||||||
|
DynamicCompletionRule(
|
||||||
|
("dict", "set"), frozenset({2}), DynamicCompletionKind.VARIABLE
|
||||||
|
),
|
||||||
|
DynamicCompletionRule(
|
||||||
|
("dict", "unset"), frozenset({2}), DynamicCompletionKind.VARIABLE
|
||||||
|
),
|
||||||
|
DynamicCompletionRule(
|
||||||
|
("dict", "update"), frozenset({2}), DynamicCompletionKind.VARIABLE
|
||||||
|
),
|
||||||
|
DynamicCompletionRule(
|
||||||
|
("dict", "with"), frozenset({2}), DynamicCompletionKind.VARIABLE
|
||||||
|
),
|
||||||
|
DynamicCompletionRule(
|
||||||
|
("global",), _REPEATED_ARGUMENTS, DynamicCompletionKind.VARIABLE
|
||||||
|
),
|
||||||
|
DynamicCompletionRule(("incr",), frozenset({1}), DynamicCompletionKind.VARIABLE),
|
||||||
|
DynamicCompletionRule(
|
||||||
|
("info", "exists"), frozenset({2}), DynamicCompletionKind.VARIABLE
|
||||||
|
),
|
||||||
|
DynamicCompletionRule(("lappend",), frozenset({1}), DynamicCompletionKind.VARIABLE),
|
||||||
|
DynamicCompletionRule(("set",), frozenset({1}), DynamicCompletionKind.VARIABLE),
|
||||||
|
DynamicCompletionRule(
|
||||||
|
("unset",), _REPEATED_ARGUMENTS, DynamicCompletionKind.VARIABLE
|
||||||
|
),
|
||||||
|
DynamicCompletionRule(
|
||||||
|
("variable",), frozenset({1}), DynamicCompletionKind.VARIABLE
|
||||||
|
),
|
||||||
|
DynamicCompletionRule(("vwait",), frozenset({1}), DynamicCompletionKind.VARIABLE),
|
||||||
|
# Procedure-taking commands.
|
||||||
|
DynamicCompletionRule(
|
||||||
|
("info", "args"), frozenset({2}), DynamicCompletionKind.PROCEDURE
|
||||||
|
),
|
||||||
|
DynamicCompletionRule(
|
||||||
|
("info", "body"), frozenset({2}), DynamicCompletionKind.PROCEDURE
|
||||||
|
),
|
||||||
|
DynamicCompletionRule(
|
||||||
|
("info", "default"), frozenset({2}), DynamicCompletionKind.PROCEDURE
|
||||||
|
),
|
||||||
|
DynamicCompletionRule(
|
||||||
|
("namespace", "origin"), frozenset({2}), DynamicCompletionKind.PROCEDURE
|
||||||
|
),
|
||||||
|
DynamicCompletionRule(("rename",), frozenset({1}), DynamicCompletionKind.PROCEDURE),
|
||||||
|
# Namespace-taking commands.
|
||||||
|
DynamicCompletionRule(
|
||||||
|
("namespace", "children"), frozenset({2}), DynamicCompletionKind.NAMESPACE
|
||||||
|
),
|
||||||
|
DynamicCompletionRule(
|
||||||
|
("namespace", "delete"),
|
||||||
|
_REPEATED_SUBCOMMAND_ARGUMENTS,
|
||||||
|
DynamicCompletionKind.NAMESPACE,
|
||||||
|
),
|
||||||
|
DynamicCompletionRule(
|
||||||
|
("namespace", "eval"), frozenset({2}), DynamicCompletionKind.NAMESPACE
|
||||||
|
),
|
||||||
|
DynamicCompletionRule(
|
||||||
|
("namespace", "exists"), frozenset({2}), DynamicCompletionKind.NAMESPACE
|
||||||
|
),
|
||||||
|
DynamicCompletionRule(
|
||||||
|
("namespace", "parent"), frozenset({2}), DynamicCompletionKind.NAMESPACE
|
||||||
|
),
|
||||||
|
# Path-taking commands. Source files are narrowed to Tcl while directories
|
||||||
|
# remain visible so users can continue navigating.
|
||||||
|
DynamicCompletionRule(("cd",), frozenset({1}), DynamicCompletionKind.PATH),
|
||||||
|
DynamicCompletionRule(
|
||||||
|
("load",), frozenset({1}), DynamicCompletionKind.PATH, (".dll", ".so", ".dylib")
|
||||||
|
),
|
||||||
|
DynamicCompletionRule(("open",), frozenset({1}), DynamicCompletionKind.PATH),
|
||||||
|
DynamicCompletionRule(
|
||||||
|
("source",), frozenset({1, 3}), DynamicCompletionKind.PATH, (".tcl",)
|
||||||
|
),
|
||||||
|
*(
|
||||||
|
DynamicCompletionRule(
|
||||||
|
("file", subcommand),
|
||||||
|
_REPEATED_SUBCOMMAND_ARGUMENTS,
|
||||||
|
DynamicCompletionKind.PATH,
|
||||||
|
)
|
||||||
|
for subcommand in ("copy", "delete", "join", "link", "mkdir", "rename")
|
||||||
|
),
|
||||||
|
*(
|
||||||
|
DynamicCompletionRule(
|
||||||
|
("file", subcommand),
|
||||||
|
frozenset({2}),
|
||||||
|
DynamicCompletionKind.PATH,
|
||||||
|
)
|
||||||
|
for subcommand in (
|
||||||
|
"atime",
|
||||||
|
"attributes",
|
||||||
|
"dirname",
|
||||||
|
"executable",
|
||||||
|
"exists",
|
||||||
|
"extension",
|
||||||
|
"isdirectory",
|
||||||
|
"isfile",
|
||||||
|
"lstat",
|
||||||
|
"mtime",
|
||||||
|
"nativename",
|
||||||
|
"normalize",
|
||||||
|
"owned",
|
||||||
|
"pathtype",
|
||||||
|
"readable",
|
||||||
|
"readlink",
|
||||||
|
"rootname",
|
||||||
|
"separator",
|
||||||
|
"size",
|
||||||
|
"split",
|
||||||
|
"stat",
|
||||||
|
"system",
|
||||||
|
"tail",
|
||||||
|
"type",
|
||||||
|
"writable",
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _snippet_item(label: str, insert_text: str, detail: str) -> CompletionItem:
|
||||||
|
return CompletionItem(
|
||||||
|
label=label,
|
||||||
|
kind=CompletionItemKind.Snippet,
|
||||||
|
detail=detail,
|
||||||
|
insert_text=insert_text,
|
||||||
|
insert_text_format=InsertTextFormat.Snippet,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
TCL_COMMAND_SNIPPET_ITEMS = {
|
||||||
|
"foreach": _snippet_item(
|
||||||
|
"foreach",
|
||||||
|
"foreach ${1:item} ${2:list} {\n\t${0}\n}",
|
||||||
|
"Tcl foreach loop",
|
||||||
|
),
|
||||||
|
"if": _snippet_item(
|
||||||
|
"if",
|
||||||
|
"if {${1:condition}} {\n\t${0}\n}",
|
||||||
|
"Tcl if block",
|
||||||
|
),
|
||||||
|
"proc": _snippet_item(
|
||||||
|
"proc",
|
||||||
|
"proc ${1:name} {${2:arguments}} {\n\t${0}\n}",
|
||||||
|
"Tcl procedure",
|
||||||
|
),
|
||||||
|
"switch": _snippet_item(
|
||||||
|
"switch",
|
||||||
|
"switch -- ${1:value} {\n\t${2:pattern} {\n\t\t${0}\n\t}\n}",
|
||||||
|
"Tcl switch block",
|
||||||
|
),
|
||||||
|
"try": _snippet_item(
|
||||||
|
"try",
|
||||||
|
"try {\n\t${1}\n} on error {${2:message} ${3:options}} {\n\t${0}\n}",
|
||||||
|
"Tcl try/on error block",
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
ARGUMENT_SNIPPETS_BY_PATH = {
|
||||||
|
("dict", "for"): _snippet_item(
|
||||||
|
"dict for loop",
|
||||||
|
"{${1:key} ${2:value}} ${3:dictionary} {\n\t${0}\n}",
|
||||||
|
"Arguments and body for dict for",
|
||||||
|
),
|
||||||
|
("foreach",): _snippet_item(
|
||||||
|
"foreach loop",
|
||||||
|
"${1:item} ${2:list} {\n\t${0}\n}",
|
||||||
|
"Arguments and body for foreach",
|
||||||
|
),
|
||||||
|
("if",): _snippet_item(
|
||||||
|
"if block",
|
||||||
|
"{${1:condition}} {\n\t${0}\n}",
|
||||||
|
"Condition and body for if",
|
||||||
|
),
|
||||||
|
("proc",): _snippet_item(
|
||||||
|
"procedure",
|
||||||
|
"${1:name} {${2:arguments}} {\n\t${0}\n}",
|
||||||
|
"Name, arguments, and body for proc",
|
||||||
|
),
|
||||||
|
("switch",): _snippet_item(
|
||||||
|
"switch block",
|
||||||
|
"-- ${1:value} {\n\t${2:pattern} {\n\t\t${0}\n\t}\n}",
|
||||||
|
"Value, patterns, and body for switch",
|
||||||
|
),
|
||||||
|
("try",): _snippet_item(
|
||||||
|
"try/on error block",
|
||||||
|
"{\n\t${1}\n} on error {${2:message} ${3:options}} {\n\t${0}\n}",
|
||||||
|
"Body and error handler for try",
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
SUBCOMMAND_SNIPPET_ITEMS = {
|
||||||
|
("dict", "for"): _snippet_item(
|
||||||
|
"for",
|
||||||
|
"for {${1:key} ${2:value}} ${3:dictionary} {\n\t${0}\n}",
|
||||||
|
"dict for loop",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
TCL_COMMAND_NAMES = tuple(
|
TCL_COMMAND_NAMES = tuple(
|
||||||
sorted(
|
sorted(
|
||||||
{path[0] for path in SUBCOMMANDS_BY_PATH}
|
{path[0] for path in SUBCOMMANDS_BY_PATH}
|
||||||
| {path[0] for path in OPTIONS_BY_PATH}
|
| {path[0] for path in OPTIONS_BY_PATH}
|
||||||
|
| set(TCL_COMMAND_SNIPPET_ITEMS)
|
||||||
|
| {rule.path[0] for rule in DYNAMIC_COMPLETION_RULES}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
TCL_COMMAND_ITEMS = tuple(
|
TCL_COMMAND_ITEMS = tuple(
|
||||||
CompletionItem(
|
TCL_COMMAND_SNIPPET_ITEMS.get(
|
||||||
label=command,
|
command,
|
||||||
kind=CompletionItemKind.Function,
|
CompletionItem(
|
||||||
detail="Tcl command",
|
label=command,
|
||||||
insert_text=command,
|
kind=CompletionItemKind.Function,
|
||||||
|
detail="Tcl command",
|
||||||
|
insert_text=command,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
for command in TCL_COMMAND_NAMES
|
for command in TCL_COMMAND_NAMES
|
||||||
)
|
)
|
||||||
@@ -420,10 +703,10 @@ def line_prefix_at_position(
|
|||||||
return line[:codepoint_offset]
|
return line[:codepoint_offset]
|
||||||
|
|
||||||
|
|
||||||
def tcl_argument_completions(
|
def tcl_argument_completion(
|
||||||
source_lines: Sequence[str], position: Position
|
source_lines: Sequence[str], position: Position
|
||||||
) -> list[CompletionItem] | None:
|
) -> TclArgumentCompletion | None:
|
||||||
"""Return subcommand, fixed-value, or option completions at ``position``.
|
"""Describe static and dynamic argument completion at ``position``.
|
||||||
|
|
||||||
``None`` means that the cursor is not at a command-specific completion
|
``None`` means that the cursor is not at a command-specific completion
|
||||||
position and the caller should fall back to normal symbol completion.
|
position and the caller should fall back to normal symbol completion.
|
||||||
@@ -445,43 +728,70 @@ def tcl_argument_completions(
|
|||||||
active_index = len(words) - 1
|
active_index = len(words) - 1
|
||||||
active_prefix = words[active_index]
|
active_prefix = words[active_index]
|
||||||
completed_path = tuple(words[:active_index])
|
completed_path = tuple(words[:active_index])
|
||||||
|
dynamic_completion = _dynamic_completion(words, active_index, active_prefix)
|
||||||
|
|
||||||
subcommands = SUBCOMMANDS_BY_PATH.get(completed_path)
|
subcommands = SUBCOMMANDS_BY_PATH.get(completed_path)
|
||||||
if subcommands is not None:
|
if subcommands is not None:
|
||||||
return _completion_items(
|
items = tuple(
|
||||||
subcommands,
|
SUBCOMMAND_SNIPPET_ITEMS.get(
|
||||||
CompletionItemKind.EnumMember,
|
(*completed_path, label),
|
||||||
f"{' '.join(completed_path)} subcommand",
|
CompletionItem(
|
||||||
|
label=label,
|
||||||
|
kind=CompletionItemKind.EnumMember,
|
||||||
|
detail=f"{' '.join(completed_path)} subcommand",
|
||||||
|
insert_text=label,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
for label in subcommands
|
||||||
)
|
)
|
||||||
|
return TclArgumentCompletion(items=items, active_prefix=active_prefix)
|
||||||
|
|
||||||
|
argument_snippet = ARGUMENT_SNIPPETS_BY_PATH.get(completed_path)
|
||||||
|
argument_items = (argument_snippet,) if argument_snippet is not None else ()
|
||||||
|
|
||||||
for (path, argument_index), values in VALUES_BY_POSITION.items():
|
for (path, argument_index), values in VALUES_BY_POSITION.items():
|
||||||
if active_index == argument_index and tuple(words[: len(path)]) == path:
|
if active_index == argument_index and tuple(words[: len(path)]) == path:
|
||||||
return _completion_items(
|
return TclArgumentCompletion(
|
||||||
values,
|
items=_completion_items(
|
||||||
CompletionItemKind.Value,
|
values,
|
||||||
f"{' '.join(path)} value",
|
CompletionItemKind.Value,
|
||||||
|
f"{' '.join(path)} value",
|
||||||
|
),
|
||||||
|
active_prefix=active_prefix,
|
||||||
)
|
)
|
||||||
|
|
||||||
for path in sorted(OPTIONS_BY_PATH, key=len, reverse=True):
|
for path in sorted(OPTIONS_BY_PATH, key=len, reverse=True):
|
||||||
if active_index < len(path) or tuple(words[: len(path)]) != path:
|
if active_index < len(path) or tuple(words[: len(path)]) != path:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
return _option_completions(
|
option_completion = _option_completion(
|
||||||
path,
|
path,
|
||||||
OPTIONS_BY_PATH[path],
|
OPTIONS_BY_PATH[path],
|
||||||
words[len(path) : active_index],
|
words[len(path) : active_index],
|
||||||
active_prefix,
|
active_prefix,
|
||||||
)
|
)
|
||||||
|
if option_completion is not None:
|
||||||
|
if active_prefix.startswith("-"):
|
||||||
|
return option_completion
|
||||||
|
return _merge_dynamic_completion(
|
||||||
|
(*argument_items, *option_completion.items),
|
||||||
|
dynamic_completion,
|
||||||
|
active_prefix,
|
||||||
|
)
|
||||||
|
|
||||||
return None
|
if argument_items:
|
||||||
|
return _merge_dynamic_completion(
|
||||||
|
argument_items, dynamic_completion, active_prefix
|
||||||
|
)
|
||||||
|
return dynamic_completion
|
||||||
|
|
||||||
|
|
||||||
def _option_completions(
|
def _option_completion(
|
||||||
path: tuple[str, ...],
|
path: tuple[str, ...],
|
||||||
options: tuple[OptionSpec, ...],
|
options: tuple[OptionSpec, ...],
|
||||||
completed_arguments: Sequence[str],
|
completed_arguments: Sequence[str],
|
||||||
active_prefix: str,
|
active_prefix: str,
|
||||||
) -> list[CompletionItem] | None:
|
) -> TclArgumentCompletion | None:
|
||||||
option_by_label = {option.label: option for option in options}
|
option_by_label = {option.label: option for option in options}
|
||||||
used_options: set[str] = set()
|
used_options: set[str] = set()
|
||||||
argument_index = 0
|
argument_index = 0
|
||||||
@@ -503,10 +813,13 @@ def _option_completions(
|
|||||||
|
|
||||||
if argument_index >= len(completed_arguments):
|
if argument_index >= len(completed_arguments):
|
||||||
if option.values:
|
if option.values:
|
||||||
return _completion_items(
|
return TclArgumentCompletion(
|
||||||
option.values,
|
items=_completion_items(
|
||||||
CompletionItemKind.Value,
|
option.values,
|
||||||
f"{option.label} value",
|
CompletionItemKind.Value,
|
||||||
|
f"{option.label} value",
|
||||||
|
),
|
||||||
|
active_prefix=active_prefix,
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
argument_index += 1
|
argument_index += 1
|
||||||
@@ -517,17 +830,53 @@ def _option_completions(
|
|||||||
remaining_options = tuple(
|
remaining_options = tuple(
|
||||||
option.label for option in options if option.label not in used_options
|
option.label for option in options if option.label not in used_options
|
||||||
)
|
)
|
||||||
return _completion_items(
|
return TclArgumentCompletion(
|
||||||
remaining_options,
|
items=_completion_items(
|
||||||
CompletionItemKind.Keyword,
|
remaining_options,
|
||||||
f"{' '.join(path)} option",
|
CompletionItemKind.Keyword,
|
||||||
|
f"{' '.join(path)} option",
|
||||||
|
),
|
||||||
|
active_prefix=active_prefix,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _dynamic_completion(
|
||||||
|
words: Sequence[str], active_index: int, active_prefix: str
|
||||||
|
) -> TclArgumentCompletion | None:
|
||||||
|
for rule in sorted(
|
||||||
|
DYNAMIC_COMPLETION_RULES, key=lambda item: len(item.path), reverse=True
|
||||||
|
):
|
||||||
|
if (
|
||||||
|
active_index in rule.argument_indices
|
||||||
|
and tuple(words[: len(rule.path)]) == rule.path
|
||||||
|
):
|
||||||
|
return TclArgumentCompletion(
|
||||||
|
dynamic_kind=rule.kind,
|
||||||
|
active_prefix=active_prefix,
|
||||||
|
path_extensions=rule.path_extensions,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _merge_dynamic_completion(
|
||||||
|
items: Sequence[CompletionItem],
|
||||||
|
dynamic_completion: TclArgumentCompletion | None,
|
||||||
|
active_prefix: str,
|
||||||
|
) -> TclArgumentCompletion:
|
||||||
|
if dynamic_completion is None:
|
||||||
|
return TclArgumentCompletion(items=tuple(items), active_prefix=active_prefix)
|
||||||
|
return TclArgumentCompletion(
|
||||||
|
items=tuple(items),
|
||||||
|
dynamic_kind=dynamic_completion.dynamic_kind,
|
||||||
|
active_prefix=active_prefix,
|
||||||
|
path_extensions=dynamic_completion.path_extensions,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _completion_items(
|
def _completion_items(
|
||||||
labels: Sequence[str], kind: CompletionItemKind, detail: str
|
labels: Sequence[str], kind: CompletionItemKind, detail: str
|
||||||
) -> list[CompletionItem]:
|
) -> tuple[CompletionItem, ...]:
|
||||||
return [
|
return tuple(
|
||||||
CompletionItem(
|
CompletionItem(
|
||||||
label=label,
|
label=label,
|
||||||
kind=kind,
|
kind=kind,
|
||||||
@@ -535,7 +884,85 @@ def _completion_items(
|
|||||||
insert_text=label,
|
insert_text=label,
|
||||||
)
|
)
|
||||||
for label in labels
|
for label in labels
|
||||||
]
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def path_completion_items(
|
||||||
|
base_directory: Path,
|
||||||
|
completion: TclArgumentCompletion,
|
||||||
|
position: Position,
|
||||||
|
*,
|
||||||
|
limit: int = 200,
|
||||||
|
) -> tuple[CompletionItem, ...]:
|
||||||
|
"""Complete one filesystem path relative to the current Tcl document."""
|
||||||
|
|
||||||
|
raw_prefix = completion.active_prefix
|
||||||
|
separator_index = max(raw_prefix.rfind("/"), raw_prefix.rfind("\\"))
|
||||||
|
typed_directory = raw_prefix[: separator_index + 1]
|
||||||
|
name_prefix = raw_prefix[separator_index + 1 :]
|
||||||
|
normalized_directory = typed_directory.replace("\\", "/")
|
||||||
|
filesystem_directory = Path(normalized_directory)
|
||||||
|
if not filesystem_directory.is_absolute():
|
||||||
|
filesystem_directory = base_directory / filesystem_directory
|
||||||
|
|
||||||
|
try:
|
||||||
|
entries = sorted(
|
||||||
|
filesystem_directory.iterdir(),
|
||||||
|
key=lambda entry: (not entry.is_dir(), entry.name.casefold()),
|
||||||
|
)
|
||||||
|
except (OSError, ValueError):
|
||||||
|
return ()
|
||||||
|
|
||||||
|
allowed_extensions = {
|
||||||
|
extension.casefold() for extension in completion.path_extensions
|
||||||
|
}
|
||||||
|
replace_start = max(
|
||||||
|
0,
|
||||||
|
position.character - len(raw_prefix.encode("utf-16-le")) // 2,
|
||||||
|
)
|
||||||
|
replace_range = Range(
|
||||||
|
start=Position(line=position.line, character=replace_start),
|
||||||
|
end=position,
|
||||||
|
)
|
||||||
|
items: list[CompletionItem] = []
|
||||||
|
|
||||||
|
for entry in entries:
|
||||||
|
if not entry.name.casefold().startswith(name_prefix.casefold()):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
is_directory = entry.is_dir()
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
if (
|
||||||
|
not is_directory
|
||||||
|
and allowed_extensions
|
||||||
|
and entry.suffix.casefold() not in allowed_extensions
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
|
escaped_name = "".join(
|
||||||
|
f"\\{character}" if character.isspace() else character
|
||||||
|
for character in entry.name
|
||||||
|
)
|
||||||
|
new_text = f"{normalized_directory}{escaped_name}"
|
||||||
|
if is_directory:
|
||||||
|
new_text += "/"
|
||||||
|
items.append(
|
||||||
|
CompletionItem(
|
||||||
|
label=new_text,
|
||||||
|
kind=(
|
||||||
|
CompletionItemKind.Folder
|
||||||
|
if is_directory
|
||||||
|
else CompletionItemKind.File
|
||||||
|
),
|
||||||
|
detail="Directory" if is_directory else "File",
|
||||||
|
text_edit=TextEdit(range=replace_range, new_text=new_text),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if len(items) >= limit:
|
||||||
|
break
|
||||||
|
|
||||||
|
return tuple(items)
|
||||||
|
|
||||||
|
|
||||||
def _current_command_segment(line_prefix: str) -> str:
|
def _current_command_segment(line_prefix: str) -> str:
|
||||||
@@ -592,41 +1019,48 @@ def _tokenize_command_segment(segment: str) -> list[str]:
|
|||||||
in_quote = False
|
in_quote = False
|
||||||
escaped = False
|
escaped = False
|
||||||
ended_with_separator = False
|
ended_with_separator = False
|
||||||
|
word_started = False
|
||||||
|
|
||||||
for char in segment:
|
for char in segment:
|
||||||
if escaped:
|
if escaped:
|
||||||
current.append(char)
|
current.append(char)
|
||||||
escaped = False
|
escaped = False
|
||||||
ended_with_separator = False
|
ended_with_separator = False
|
||||||
|
word_started = True
|
||||||
continue
|
continue
|
||||||
if char == "\\":
|
if char == "\\":
|
||||||
current.append(char)
|
current.append(char)
|
||||||
escaped = True
|
escaped = True
|
||||||
ended_with_separator = False
|
ended_with_separator = False
|
||||||
|
word_started = True
|
||||||
continue
|
continue
|
||||||
if char == '"' and brace_depth == 0:
|
if char == '"' and brace_depth == 0:
|
||||||
in_quote = not in_quote
|
in_quote = not in_quote
|
||||||
ended_with_separator = False
|
ended_with_separator = False
|
||||||
|
word_started = True
|
||||||
continue
|
continue
|
||||||
if not in_quote and char == "{":
|
if not in_quote and char == "{":
|
||||||
brace_depth += 1
|
brace_depth += 1
|
||||||
ended_with_separator = False
|
ended_with_separator = False
|
||||||
|
word_started = True
|
||||||
continue
|
continue
|
||||||
if not in_quote and char == "}" and brace_depth:
|
if not in_quote and char == "}" and brace_depth:
|
||||||
brace_depth -= 1
|
brace_depth -= 1
|
||||||
ended_with_separator = False
|
ended_with_separator = False
|
||||||
continue
|
continue
|
||||||
if char.isspace() and not in_quote and brace_depth == 0:
|
if char.isspace() and not in_quote and brace_depth == 0:
|
||||||
if current:
|
if current or word_started:
|
||||||
words.append("".join(current))
|
words.append("".join(current))
|
||||||
current = []
|
current = []
|
||||||
|
word_started = False
|
||||||
ended_with_separator = bool(words)
|
ended_with_separator = bool(words)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
current.append(char)
|
current.append(char)
|
||||||
ended_with_separator = False
|
ended_with_separator = False
|
||||||
|
word_started = True
|
||||||
|
|
||||||
if current:
|
if current or word_started:
|
||||||
words.append("".join(current))
|
words.append("".join(current))
|
||||||
elif ended_with_separator:
|
elif ended_with_separator:
|
||||||
words.append("")
|
words.append("")
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Dict, List, Set
|
|
||||||
|
|
||||||
from tclint.syntax_tree import Command, Node, Script
|
from tclint.syntax_tree import Command, Node, Script
|
||||||
|
from tclint.syntax_tree import List as TclList
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -18,13 +18,14 @@ def _normalize_var_name(raw_name: str | None) -> str | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
base = raw_name.split("(", 1)[0]
|
base = raw_name.split("(", 1)[0]
|
||||||
if base.startswith("::"):
|
base = base.removeprefix("::")
|
||||||
base = base[2:]
|
|
||||||
|
|
||||||
return base or None
|
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:
|
Parse Tcl source text and build:
|
||||||
- globals: set of variable names considered global suggestions
|
- 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.
|
_ = source # Kept for signature compatibility with callers.
|
||||||
|
|
||||||
globals_set: Set[str] = set()
|
globals_set: set[str] = set()
|
||||||
procs: Dict[str, Set[str]] = {}
|
procs: dict[str, set[str]] = {}
|
||||||
proc_ranges: List[ProcRange] = []
|
proc_ranges: list[ProcRange] = []
|
||||||
|
|
||||||
if tree is None:
|
if tree is None:
|
||||||
return globals_set, procs, proc_ranges
|
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):
|
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))
|
proc_name = _normalize_var_name(getattr(node.args[0], "contents", None))
|
||||||
if proc_name is not 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(
|
proc_ranges.append(
|
||||||
ProcRange(
|
ProcRange(
|
||||||
name=proc_name,
|
name=proc_name,
|
||||||
|
|||||||
@@ -19,7 +19,11 @@ from tools.completion_items import (
|
|||||||
CompletionContext,
|
CompletionContext,
|
||||||
completion_context,
|
completion_context,
|
||||||
)
|
)
|
||||||
from tools.tcl_command_completion import tcl_argument_completions
|
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:
|
def _position_after(source: str, token: str, occurrence: int = 0) -> lsp.Position:
|
||||||
@@ -93,15 +97,19 @@ def _complete(document: TextDocument, position: lsp.Position):
|
|||||||
|
|
||||||
|
|
||||||
def _argument_completion_labels(source: str) -> set[str] | None:
|
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")
|
lines = source.split("\n")
|
||||||
character = len(lines[-1].encode("utf-16-le")) // 2
|
character = len(lines[-1].encode("utf-16-le")) // 2
|
||||||
items = tcl_argument_completions(
|
return tcl_argument_completion(
|
||||||
lines,
|
lines,
|
||||||
lsp.Position(line=len(lines) - 1, character=character),
|
lsp.Position(line=len(lines) - 1, character=character),
|
||||||
)
|
)
|
||||||
if items is None:
|
|
||||||
return None
|
|
||||||
return {item.label for item in items}
|
|
||||||
|
|
||||||
|
|
||||||
def test_variable_completion_filters_and_ranks_candidates(tmp_path: Path, monkeypatch):
|
def test_variable_completion_filters_and_ranks_candidates(tmp_path: Path, monkeypatch):
|
||||||
@@ -308,3 +316,172 @@ def test_space_trigger_does_not_open_broad_fallback_completion(
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert result.items == []
|
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user