feat(lsp): add Tcl command-aware completion and integration
- Introduced a Tcl command-aware completion engine and wired to LSP. - Added a new module with Tcl commands, options, and contextual matching. - Updated completion to favor command-aware items and args.
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
- Add incoming and outgoing call hierarchy for custom TCL procedures and MOM event handlers
|
- Add incoming and outgoing call hierarchy for custom TCL procedures and MOM event handlers
|
||||||
- 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
|
||||||
- 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,7 @@ 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 or commands based on cursor context
|
- **Context-aware Completion** - Prioritizes local symbols and suggests variables, commands, Tcl subcommands, and valid options based on cursor context
|
||||||
- **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 +101,7 @@ 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
|
- Context-aware completion with local symbols ranked before workspace and built-in symbols, plus Tcl subcommands and options such as `string compare -nocase`
|
||||||
- Remote NX Tcl debugging with breakpoints and full stepping
|
- Remote NX Tcl debugging with breakpoints and full stepping
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|||||||
@@ -38,13 +38,18 @@ update_sys_path(
|
|||||||
# Imports needed for the language server goes below this.
|
# Imports needed for the language server goes below this.
|
||||||
# **********************************************************
|
# **********************************************************
|
||||||
# pylint: disable=wrong-import-position,import-error
|
# pylint: disable=wrong-import-position,import-error
|
||||||
import lsp_jsonrpc as jsonrpc
|
|
||||||
import lsprotocol.types as lsp
|
import lsprotocol.types as lsp
|
||||||
from common.load_data import standard_items
|
|
||||||
from lsp_tclserver import TclLanguageServer
|
|
||||||
from pygls import uris
|
from pygls import uris
|
||||||
from pygls.workspace.text_document import TextDocument
|
from pygls.workspace.text_document import TextDocument
|
||||||
from tools.completion_items import completion_context, ranked_completion_items
|
|
||||||
|
import lsp_jsonrpc as jsonrpc
|
||||||
|
from common.load_data import standard_items
|
||||||
|
from lsp_tclserver import TclLanguageServer
|
||||||
|
from tools.completion_items import (
|
||||||
|
CompletionContext,
|
||||||
|
completion_context,
|
||||||
|
ranked_completion_items,
|
||||||
|
)
|
||||||
from tools.folding_ranges import build_folding_ranges
|
from tools.folding_ranges import build_folding_ranges
|
||||||
from tools.inlay_hint import (
|
from tools.inlay_hint import (
|
||||||
InlayHintGenerator,
|
InlayHintGenerator,
|
||||||
@@ -69,6 +74,11 @@ from tools.semantic_tokens import (
|
|||||||
_Highlighter,
|
_Highlighter,
|
||||||
)
|
)
|
||||||
from tools.signature_help import build_signature_help
|
from tools.signature_help import build_signature_help
|
||||||
|
from tools.tcl_command_completion import (
|
||||||
|
TCL_COMMAND_ITEMS,
|
||||||
|
TCL_COMMAND_NAMES,
|
||||||
|
tcl_argument_completions,
|
||||||
|
)
|
||||||
|
|
||||||
WORKSPACE_SETTINGS = {}
|
WORKSPACE_SETTINGS = {}
|
||||||
GLOBAL_SETTINGS = {}
|
GLOBAL_SETTINGS = {}
|
||||||
@@ -82,10 +92,12 @@ LSP_SERVER = TclLanguageServer(
|
|||||||
BUILTIN_PROC_NAMES = {
|
BUILTIN_PROC_NAMES = {
|
||||||
item.label
|
item.label
|
||||||
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)
|
||||||
BUILTIN_VARIABLE_NAMES = {item.label for item in standard_items.nx_variables}
|
BUILTIN_VARIABLE_NAMES = {item.label for item in standard_items.nx_variables}
|
||||||
|
_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
|
standard_items.tcl_keyword_list
|
||||||
|
+ [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
|
||||||
)
|
)
|
||||||
@@ -250,16 +262,40 @@ 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
|
||||||
|
source_lines = LSP_SERVER.get_lines(doc)
|
||||||
|
context = completion_context(source_lines, position)
|
||||||
|
|
||||||
|
# Variable completion wins inside command arguments. Otherwise prefer the
|
||||||
|
# narrow command grammar when the cursor is at a known subcommand/option.
|
||||||
|
if context != CompletionContext.VARIABLE:
|
||||||
|
argument_items = tcl_argument_completions(source_lines, position)
|
||||||
|
if argument_items is not None:
|
||||||
|
items = ranked_completion_items(
|
||||||
|
((0, item) for item in argument_items),
|
||||||
|
CompletionContext.GENERAL,
|
||||||
|
)
|
||||||
|
return lsp.CompletionList(is_incomplete=False, items=items)
|
||||||
|
|
||||||
|
# Space and dash are registered only to open command-aware suggestions.
|
||||||
|
# Do not display the broad fallback list when such a trigger has no match.
|
||||||
|
if (
|
||||||
|
params.context is not None
|
||||||
|
and params.context.trigger_kind
|
||||||
|
== lsp.CompletionTriggerKind.TriggerCharacter
|
||||||
|
and params.context.trigger_character in {" ", "-"}
|
||||||
|
):
|
||||||
|
return lsp.CompletionList(is_incomplete=False, items=[])
|
||||||
|
|
||||||
tree = LSP_SERVER.get_tree(doc)
|
tree = LSP_SERVER.get_tree(doc)
|
||||||
globals_set, procs_locals, proc_ranges = LSP_SERVER.variable_index_for_document(
|
globals_set, procs_locals, proc_ranges = LSP_SERVER.variable_index_for_document(
|
||||||
doc, tree
|
doc, tree
|
||||||
)
|
)
|
||||||
|
|
||||||
position = params.position
|
|
||||||
local_names: set[str] = set()
|
local_names: set[str] = set()
|
||||||
for proc_range in proc_ranges:
|
for proc_range in proc_ranges:
|
||||||
end_line = proc_range.end_line or proc_range.start_line
|
end_line = proc_range.end_line or proc_range.start_line
|
||||||
@@ -302,7 +338,6 @@ def on_completion(params: lsp.CompletionParams) -> lsp.CompletionList:
|
|||||||
candidates.extend((priority, item) for item in items_by_file[item_path])
|
candidates.extend((priority, item) for item in items_by_file[item_path])
|
||||||
|
|
||||||
candidates.extend((300, item) for item in STATIC_COMPLETION_ITEMS)
|
candidates.extend((300, item) for item in STATIC_COMPLETION_ITEMS)
|
||||||
context = completion_context(LSP_SERVER.get_lines(doc), position)
|
|
||||||
items = ranked_completion_items(candidates, context)
|
items = ranked_completion_items(candidates, context)
|
||||||
return lsp.CompletionList(is_incomplete=False, items=items)
|
return lsp.CompletionList(is_incomplete=False, items=items)
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ from collections.abc import Iterable, Sequence
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
import lsprotocol.types as lsp
|
import lsprotocol.types as lsp
|
||||||
from common.load_data import standard_items
|
|
||||||
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 tools.tcl_command_completion import line_prefix_at_position
|
||||||
|
|
||||||
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}
|
||||||
|
|
||||||
@@ -35,27 +37,12 @@ _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;\[\]\{\}]*$")
|
||||||
|
|
||||||
|
|
||||||
def _codepoint_offset(line: str, utf16_offset: int) -> int:
|
|
||||||
"""Translate an LSP UTF-16 character offset into a Python string offset."""
|
|
||||||
if utf16_offset <= 0:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
units = 0
|
|
||||||
for offset, character in enumerate(line):
|
|
||||||
units += 2 if ord(character) > 0xFFFF else 1
|
|
||||||
if units >= utf16_offset:
|
|
||||||
return offset + 1
|
|
||||||
return len(line)
|
|
||||||
|
|
||||||
|
|
||||||
def completion_context(
|
def completion_context(
|
||||||
source_lines: Sequence[str], position: lsp.Position
|
source_lines: Sequence[str], position: lsp.Position
|
||||||
) -> CompletionContext:
|
) -> CompletionContext:
|
||||||
if position.line < 0 or position.line >= len(source_lines):
|
prefix = line_prefix_at_position(source_lines, position)
|
||||||
|
if prefix is None:
|
||||||
return CompletionContext.GENERAL
|
return CompletionContext.GENERAL
|
||||||
|
|
||||||
line = source_lines[position.line]
|
|
||||||
prefix = line[: _codepoint_offset(line, position.character)]
|
|
||||||
if _VARIABLE_PREFIX_RE.search(prefix):
|
if _VARIABLE_PREFIX_RE.search(prefix):
|
||||||
return CompletionContext.VARIABLE
|
return CompletionContext.VARIABLE
|
||||||
if _COMMAND_PREFIX_RE.search(prefix):
|
if _COMMAND_PREFIX_RE.search(prefix):
|
||||||
|
|||||||
@@ -0,0 +1,650 @@
|
|||||||
|
"""Command-aware completion data and cursor parsing for Tcl commands."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Sequence
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from lsprotocol.types import CompletionItem, CompletionItemKind, Position
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class OptionSpec:
|
||||||
|
"""A command option and whether it consumes the following word."""
|
||||||
|
|
||||||
|
label: str
|
||||||
|
takes_value: bool = False
|
||||||
|
values: tuple[str, ...] = ()
|
||||||
|
|
||||||
|
|
||||||
|
STRING_CLASSES = (
|
||||||
|
"alnum",
|
||||||
|
"alpha",
|
||||||
|
"ascii",
|
||||||
|
"boolean",
|
||||||
|
"control",
|
||||||
|
"digit",
|
||||||
|
"double",
|
||||||
|
"entier",
|
||||||
|
"false",
|
||||||
|
"graph",
|
||||||
|
"integer",
|
||||||
|
"list",
|
||||||
|
"lower",
|
||||||
|
"print",
|
||||||
|
"punct",
|
||||||
|
"space",
|
||||||
|
"true",
|
||||||
|
"upper",
|
||||||
|
"wideinteger",
|
||||||
|
"wordchar",
|
||||||
|
"xdigit",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Keys are complete command paths. Their values are valid words immediately
|
||||||
|
# following that path. Keeping this declarative makes Tcl-version additions easy.
|
||||||
|
SUBCOMMANDS_BY_PATH: dict[tuple[str, ...], tuple[str, ...]] = {
|
||||||
|
("array",): (
|
||||||
|
"anymore",
|
||||||
|
"donesearch",
|
||||||
|
"exists",
|
||||||
|
"get",
|
||||||
|
"names",
|
||||||
|
"nextelement",
|
||||||
|
"set",
|
||||||
|
"size",
|
||||||
|
"startsearch",
|
||||||
|
"statistics",
|
||||||
|
"unset",
|
||||||
|
),
|
||||||
|
("binary",): ("decode", "encode", "format", "scan"),
|
||||||
|
("binary", "decode"): ("base64", "hex", "uuencode"),
|
||||||
|
("binary", "encode"): ("base64", "hex", "uuencode"),
|
||||||
|
("chan",): (
|
||||||
|
"blocked",
|
||||||
|
"close",
|
||||||
|
"configure",
|
||||||
|
"copy",
|
||||||
|
"create",
|
||||||
|
"eof",
|
||||||
|
"event",
|
||||||
|
"flush",
|
||||||
|
"gets",
|
||||||
|
"names",
|
||||||
|
"pending",
|
||||||
|
"pipe",
|
||||||
|
"pop",
|
||||||
|
"postevent",
|
||||||
|
"push",
|
||||||
|
"puts",
|
||||||
|
"read",
|
||||||
|
"seek",
|
||||||
|
"tell",
|
||||||
|
"truncate",
|
||||||
|
),
|
||||||
|
("clock",): (
|
||||||
|
"add",
|
||||||
|
"clicks",
|
||||||
|
"format",
|
||||||
|
"microseconds",
|
||||||
|
"milliseconds",
|
||||||
|
"scan",
|
||||||
|
"seconds",
|
||||||
|
),
|
||||||
|
("dict",): (
|
||||||
|
"append",
|
||||||
|
"create",
|
||||||
|
"exists",
|
||||||
|
"filter",
|
||||||
|
"for",
|
||||||
|
"get",
|
||||||
|
"incr",
|
||||||
|
"info",
|
||||||
|
"keys",
|
||||||
|
"lappend",
|
||||||
|
"map",
|
||||||
|
"merge",
|
||||||
|
"remove",
|
||||||
|
"replace",
|
||||||
|
"set",
|
||||||
|
"size",
|
||||||
|
"unset",
|
||||||
|
"update",
|
||||||
|
"values",
|
||||||
|
"with",
|
||||||
|
),
|
||||||
|
("dict", "filter"): ("key", "script", "value"),
|
||||||
|
("encoding",): ("convertfrom", "convertto", "dirs", "names", "system"),
|
||||||
|
("file",): (
|
||||||
|
"atime",
|
||||||
|
"attributes",
|
||||||
|
"channels",
|
||||||
|
"copy",
|
||||||
|
"delete",
|
||||||
|
"dirname",
|
||||||
|
"executable",
|
||||||
|
"exists",
|
||||||
|
"extension",
|
||||||
|
"isdirectory",
|
||||||
|
"isfile",
|
||||||
|
"join",
|
||||||
|
"link",
|
||||||
|
"lstat",
|
||||||
|
"mkdir",
|
||||||
|
"mtime",
|
||||||
|
"nativename",
|
||||||
|
"normalize",
|
||||||
|
"owned",
|
||||||
|
"pathtype",
|
||||||
|
"readable",
|
||||||
|
"readlink",
|
||||||
|
"rename",
|
||||||
|
"rootname",
|
||||||
|
"separator",
|
||||||
|
"size",
|
||||||
|
"split",
|
||||||
|
"stat",
|
||||||
|
"system",
|
||||||
|
"tail",
|
||||||
|
"tempfile",
|
||||||
|
"type",
|
||||||
|
"volumes",
|
||||||
|
"writable",
|
||||||
|
),
|
||||||
|
("info",): (
|
||||||
|
"args",
|
||||||
|
"body",
|
||||||
|
"class",
|
||||||
|
"cmdcount",
|
||||||
|
"commands",
|
||||||
|
"complete",
|
||||||
|
"coroutine",
|
||||||
|
"default",
|
||||||
|
"errorstack",
|
||||||
|
"exists",
|
||||||
|
"frame",
|
||||||
|
"functions",
|
||||||
|
"globals",
|
||||||
|
"hostname",
|
||||||
|
"level",
|
||||||
|
"library",
|
||||||
|
"loaded",
|
||||||
|
"locals",
|
||||||
|
"nameofexecutable",
|
||||||
|
"object",
|
||||||
|
"patchlevel",
|
||||||
|
"procs",
|
||||||
|
"script",
|
||||||
|
"sharedlibextension",
|
||||||
|
"tclversion",
|
||||||
|
"vars",
|
||||||
|
),
|
||||||
|
("namespace",): (
|
||||||
|
"children",
|
||||||
|
"code",
|
||||||
|
"current",
|
||||||
|
"delete",
|
||||||
|
"ensemble",
|
||||||
|
"eval",
|
||||||
|
"exists",
|
||||||
|
"export",
|
||||||
|
"forget",
|
||||||
|
"import",
|
||||||
|
"inscope",
|
||||||
|
"origin",
|
||||||
|
"parent",
|
||||||
|
"path",
|
||||||
|
"qualifiers",
|
||||||
|
"tail",
|
||||||
|
"unknown",
|
||||||
|
"upvar",
|
||||||
|
"which",
|
||||||
|
),
|
||||||
|
("namespace", "ensemble"): ("configure", "create", "exists"),
|
||||||
|
("package",): (
|
||||||
|
"forget",
|
||||||
|
"ifneeded",
|
||||||
|
"names",
|
||||||
|
"prefer",
|
||||||
|
"present",
|
||||||
|
"provide",
|
||||||
|
"require",
|
||||||
|
"unknown",
|
||||||
|
"vcompare",
|
||||||
|
"versions",
|
||||||
|
"vsatisfies",
|
||||||
|
),
|
||||||
|
("string",): (
|
||||||
|
"bytelength",
|
||||||
|
"cat",
|
||||||
|
"compare",
|
||||||
|
"equal",
|
||||||
|
"first",
|
||||||
|
"index",
|
||||||
|
"is",
|
||||||
|
"last",
|
||||||
|
"length",
|
||||||
|
"map",
|
||||||
|
"match",
|
||||||
|
"range",
|
||||||
|
"repeat",
|
||||||
|
"replace",
|
||||||
|
"reverse",
|
||||||
|
"tolower",
|
||||||
|
"totitle",
|
||||||
|
"toupper",
|
||||||
|
"trim",
|
||||||
|
"trimleft",
|
||||||
|
"trimright",
|
||||||
|
"wordend",
|
||||||
|
"wordstart",
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _options(*labels: str) -> tuple[OptionSpec, ...]:
|
||||||
|
return tuple(OptionSpec(label) for label in labels)
|
||||||
|
|
||||||
|
|
||||||
|
OPTIONS_BY_PATH: dict[tuple[str, ...], tuple[OptionSpec, ...]] = {
|
||||||
|
("binary", "decode", "base64"): (OptionSpec("-strict"),),
|
||||||
|
("binary", "encode", "base64"): (
|
||||||
|
OptionSpec("-maxlen", takes_value=True),
|
||||||
|
OptionSpec("-wrapchar", takes_value=True),
|
||||||
|
),
|
||||||
|
("clock", "format"): (
|
||||||
|
OptionSpec("-format", takes_value=True),
|
||||||
|
OptionSpec("-gmt", takes_value=True, values=("0", "1")),
|
||||||
|
OptionSpec("-locale", takes_value=True),
|
||||||
|
OptionSpec("-timezone", takes_value=True),
|
||||||
|
),
|
||||||
|
("clock", "scan"): (
|
||||||
|
OptionSpec("-base", takes_value=True),
|
||||||
|
OptionSpec("-format", takes_value=True),
|
||||||
|
OptionSpec("-gmt", takes_value=True, values=("0", "1")),
|
||||||
|
OptionSpec("-locale", takes_value=True),
|
||||||
|
OptionSpec("-timezone", takes_value=True),
|
||||||
|
),
|
||||||
|
("exec",): _options("-ignorestderr", "-keepnewline", "--"),
|
||||||
|
("file", "copy"): _options("-force", "--"),
|
||||||
|
("file", "delete"): _options("-force", "--"),
|
||||||
|
("file", "link"): _options("-symbolic", "-hard"),
|
||||||
|
("file", "rename"): _options("-force", "--"),
|
||||||
|
("glob",): (
|
||||||
|
OptionSpec("-directory", takes_value=True),
|
||||||
|
OptionSpec("-join"),
|
||||||
|
OptionSpec("-nocomplain"),
|
||||||
|
OptionSpec("-path", takes_value=True),
|
||||||
|
OptionSpec("-tails"),
|
||||||
|
OptionSpec("-types", takes_value=True),
|
||||||
|
OptionSpec("--"),
|
||||||
|
),
|
||||||
|
("lsearch",): (
|
||||||
|
OptionSpec("-all"),
|
||||||
|
OptionSpec("-ascii"),
|
||||||
|
OptionSpec("-bisect"),
|
||||||
|
OptionSpec("-decreasing"),
|
||||||
|
OptionSpec("-dictionary"),
|
||||||
|
OptionSpec("-exact"),
|
||||||
|
OptionSpec("-glob"),
|
||||||
|
OptionSpec("-increasing"),
|
||||||
|
OptionSpec("-index", takes_value=True),
|
||||||
|
OptionSpec("-inline"),
|
||||||
|
OptionSpec("-integer"),
|
||||||
|
OptionSpec("-nocase"),
|
||||||
|
OptionSpec("-not"),
|
||||||
|
OptionSpec("-real"),
|
||||||
|
OptionSpec("-regexp"),
|
||||||
|
OptionSpec("-sorted"),
|
||||||
|
OptionSpec("-start", takes_value=True),
|
||||||
|
OptionSpec("-subindices"),
|
||||||
|
),
|
||||||
|
("lsort",): (
|
||||||
|
OptionSpec("-ascii"),
|
||||||
|
OptionSpec("-command", takes_value=True),
|
||||||
|
OptionSpec("-decreasing"),
|
||||||
|
OptionSpec("-dictionary"),
|
||||||
|
OptionSpec("-increasing"),
|
||||||
|
OptionSpec("-index", takes_value=True),
|
||||||
|
OptionSpec("-indices"),
|
||||||
|
OptionSpec("-integer"),
|
||||||
|
OptionSpec("-nocase"),
|
||||||
|
OptionSpec("-real"),
|
||||||
|
OptionSpec("-stride", takes_value=True),
|
||||||
|
OptionSpec("-unique"),
|
||||||
|
),
|
||||||
|
("namespace", "which"): _options("-command", "-variable"),
|
||||||
|
("package", "present"): _options("-exact"),
|
||||||
|
("package", "require"): _options("-exact"),
|
||||||
|
("puts",): _options("-nonewline"),
|
||||||
|
("regexp",): (
|
||||||
|
OptionSpec("-about"),
|
||||||
|
OptionSpec("-all"),
|
||||||
|
OptionSpec("-expanded"),
|
||||||
|
OptionSpec("-indices"),
|
||||||
|
OptionSpec("-inline"),
|
||||||
|
OptionSpec("-line"),
|
||||||
|
OptionSpec("-lineanchor"),
|
||||||
|
OptionSpec("-linestop"),
|
||||||
|
OptionSpec("-nocase"),
|
||||||
|
OptionSpec("-start", takes_value=True),
|
||||||
|
OptionSpec("--"),
|
||||||
|
),
|
||||||
|
("regsub",): (
|
||||||
|
OptionSpec("-all"),
|
||||||
|
OptionSpec("-command"),
|
||||||
|
OptionSpec("-expanded"),
|
||||||
|
OptionSpec("-line"),
|
||||||
|
OptionSpec("-lineanchor"),
|
||||||
|
OptionSpec("-linestop"),
|
||||||
|
OptionSpec("-nocase"),
|
||||||
|
OptionSpec("-start", takes_value=True),
|
||||||
|
OptionSpec("--"),
|
||||||
|
),
|
||||||
|
("return",): (
|
||||||
|
OptionSpec("-code", takes_value=True),
|
||||||
|
OptionSpec("-errorcode", takes_value=True),
|
||||||
|
OptionSpec("-errorinfo", takes_value=True),
|
||||||
|
OptionSpec("-errorstack", takes_value=True),
|
||||||
|
OptionSpec("-level", takes_value=True),
|
||||||
|
OptionSpec("-options", takes_value=True),
|
||||||
|
),
|
||||||
|
("source",): (OptionSpec("-encoding", takes_value=True),),
|
||||||
|
("string", "compare"): (
|
||||||
|
OptionSpec("-nocase"),
|
||||||
|
OptionSpec("-length", takes_value=True),
|
||||||
|
),
|
||||||
|
("string", "equal"): (
|
||||||
|
OptionSpec("-nocase"),
|
||||||
|
OptionSpec("-length", takes_value=True),
|
||||||
|
),
|
||||||
|
("string", "map"): _options("-nocase"),
|
||||||
|
("string", "match"): _options("-nocase"),
|
||||||
|
("switch",): (
|
||||||
|
OptionSpec("-exact"),
|
||||||
|
OptionSpec("-glob"),
|
||||||
|
OptionSpec("-indexvar", takes_value=True),
|
||||||
|
OptionSpec("-matchvar", takes_value=True),
|
||||||
|
OptionSpec("-nocase"),
|
||||||
|
OptionSpec("-regexp"),
|
||||||
|
OptionSpec("--"),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
# ``string is`` takes its class before its options, so each class is a concrete
|
||||||
|
# command path for the generic option resolver below.
|
||||||
|
for _string_class in STRING_CLASSES:
|
||||||
|
OPTIONS_BY_PATH[("string", "is", _string_class)] = (
|
||||||
|
OptionSpec("-strict"),
|
||||||
|
OptionSpec("-failindex", takes_value=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
VALUES_BY_POSITION: dict[tuple[tuple[str, ...], int], tuple[str, ...]] = {
|
||||||
|
(("array", "names"), 3): ("-exact", "-glob", "-regexp"),
|
||||||
|
(("string", "is"), 2): STRING_CLASSES,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TCL_COMMAND_NAMES = tuple(
|
||||||
|
sorted(
|
||||||
|
{path[0] for path in SUBCOMMANDS_BY_PATH}
|
||||||
|
| {path[0] for path in OPTIONS_BY_PATH}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
TCL_COMMAND_ITEMS = tuple(
|
||||||
|
CompletionItem(
|
||||||
|
label=command,
|
||||||
|
kind=CompletionItemKind.Function,
|
||||||
|
detail="Tcl command",
|
||||||
|
insert_text=command,
|
||||||
|
)
|
||||||
|
for command in TCL_COMMAND_NAMES
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def line_prefix_at_position(
|
||||||
|
source_lines: Sequence[str], position: Position
|
||||||
|
) -> str | None:
|
||||||
|
"""Return the current line before an LSP UTF-16 position."""
|
||||||
|
|
||||||
|
if position.line < 0 or position.line >= len(source_lines):
|
||||||
|
return None
|
||||||
|
|
||||||
|
line = source_lines[position.line]
|
||||||
|
codepoint_offset = _codepoint_offset(line, position.character)
|
||||||
|
if codepoint_offset is None:
|
||||||
|
return None
|
||||||
|
return line[:codepoint_offset]
|
||||||
|
|
||||||
|
|
||||||
|
def tcl_argument_completions(
|
||||||
|
source_lines: Sequence[str], position: Position
|
||||||
|
) -> list[CompletionItem] | None:
|
||||||
|
"""Return subcommand, fixed-value, or option completions at ``position``.
|
||||||
|
|
||||||
|
``None`` means that the cursor is not at a command-specific completion
|
||||||
|
position and the caller should fall back to normal symbol completion.
|
||||||
|
"""
|
||||||
|
|
||||||
|
line_prefix = line_prefix_at_position(source_lines, position)
|
||||||
|
if line_prefix is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
segment = _current_command_segment(line_prefix)
|
||||||
|
if not segment.strip() or segment.lstrip().startswith("#"):
|
||||||
|
return None
|
||||||
|
|
||||||
|
words = _tokenize_command_segment(segment)
|
||||||
|
if not words:
|
||||||
|
return None
|
||||||
|
|
||||||
|
words[0] = words[0].removeprefix("::")
|
||||||
|
active_index = len(words) - 1
|
||||||
|
active_prefix = words[active_index]
|
||||||
|
completed_path = tuple(words[:active_index])
|
||||||
|
|
||||||
|
subcommands = SUBCOMMANDS_BY_PATH.get(completed_path)
|
||||||
|
if subcommands is not None:
|
||||||
|
return _completion_items(
|
||||||
|
subcommands,
|
||||||
|
CompletionItemKind.EnumMember,
|
||||||
|
f"{' '.join(completed_path)} subcommand",
|
||||||
|
)
|
||||||
|
|
||||||
|
for (path, argument_index), values in VALUES_BY_POSITION.items():
|
||||||
|
if active_index == argument_index and tuple(words[: len(path)]) == path:
|
||||||
|
return _completion_items(
|
||||||
|
values,
|
||||||
|
CompletionItemKind.Value,
|
||||||
|
f"{' '.join(path)} value",
|
||||||
|
)
|
||||||
|
|
||||||
|
for path in sorted(OPTIONS_BY_PATH, key=len, reverse=True):
|
||||||
|
if active_index < len(path) or tuple(words[: len(path)]) != path:
|
||||||
|
continue
|
||||||
|
|
||||||
|
return _option_completions(
|
||||||
|
path,
|
||||||
|
OPTIONS_BY_PATH[path],
|
||||||
|
words[len(path) : active_index],
|
||||||
|
active_prefix,
|
||||||
|
)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _option_completions(
|
||||||
|
path: tuple[str, ...],
|
||||||
|
options: tuple[OptionSpec, ...],
|
||||||
|
completed_arguments: Sequence[str],
|
||||||
|
active_prefix: str,
|
||||||
|
) -> list[CompletionItem] | None:
|
||||||
|
option_by_label = {option.label: option for option in options}
|
||||||
|
used_options: set[str] = set()
|
||||||
|
argument_index = 0
|
||||||
|
|
||||||
|
while argument_index < len(completed_arguments):
|
||||||
|
argument = completed_arguments[argument_index]
|
||||||
|
if argument == "--":
|
||||||
|
return None
|
||||||
|
|
||||||
|
option = option_by_label.get(argument)
|
||||||
|
if option is None:
|
||||||
|
# Tcl options precede normal operands for the commands covered here.
|
||||||
|
return None
|
||||||
|
|
||||||
|
used_options.add(option.label)
|
||||||
|
argument_index += 1
|
||||||
|
if not option.takes_value:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if argument_index >= len(completed_arguments):
|
||||||
|
if option.values:
|
||||||
|
return _completion_items(
|
||||||
|
option.values,
|
||||||
|
CompletionItemKind.Value,
|
||||||
|
f"{option.label} value",
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
argument_index += 1
|
||||||
|
|
||||||
|
if active_prefix and not active_prefix.startswith("-"):
|
||||||
|
return None
|
||||||
|
|
||||||
|
remaining_options = tuple(
|
||||||
|
option.label for option in options if option.label not in used_options
|
||||||
|
)
|
||||||
|
return _completion_items(
|
||||||
|
remaining_options,
|
||||||
|
CompletionItemKind.Keyword,
|
||||||
|
f"{' '.join(path)} option",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _completion_items(
|
||||||
|
labels: Sequence[str], kind: CompletionItemKind, detail: str
|
||||||
|
) -> list[CompletionItem]:
|
||||||
|
return [
|
||||||
|
CompletionItem(
|
||||||
|
label=label,
|
||||||
|
kind=kind,
|
||||||
|
detail=detail,
|
||||||
|
insert_text=label,
|
||||||
|
)
|
||||||
|
for label in labels
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _current_command_segment(line_prefix: str) -> str:
|
||||||
|
"""Select the innermost unfinished command from a line prefix."""
|
||||||
|
|
||||||
|
brace_starts: list[int] = []
|
||||||
|
bracket_starts: list[int] = []
|
||||||
|
last_command_delimiter = -1
|
||||||
|
in_quote = False
|
||||||
|
escaped = False
|
||||||
|
|
||||||
|
for index, char in enumerate(line_prefix):
|
||||||
|
if escaped:
|
||||||
|
escaped = False
|
||||||
|
continue
|
||||||
|
if char == "\\":
|
||||||
|
escaped = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
if char == '"' and not brace_starts:
|
||||||
|
in_quote = not in_quote
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not in_quote:
|
||||||
|
if char == "{":
|
||||||
|
brace_starts.append(index)
|
||||||
|
continue
|
||||||
|
if char == "}" and brace_starts:
|
||||||
|
brace_starts.pop()
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not brace_starts:
|
||||||
|
if char == "[":
|
||||||
|
bracket_starts.append(index)
|
||||||
|
continue
|
||||||
|
if char == "]" and bracket_starts:
|
||||||
|
bracket_starts.pop()
|
||||||
|
continue
|
||||||
|
if char == ";" and not in_quote:
|
||||||
|
last_command_delimiter = index
|
||||||
|
|
||||||
|
starts = [last_command_delimiter + 1]
|
||||||
|
if brace_starts:
|
||||||
|
starts.append(brace_starts[-1] + 1)
|
||||||
|
if bracket_starts:
|
||||||
|
starts.append(bracket_starts[-1] + 1)
|
||||||
|
return line_prefix[max(starts) :]
|
||||||
|
|
||||||
|
|
||||||
|
def _tokenize_command_segment(segment: str) -> list[str]:
|
||||||
|
words: list[str] = []
|
||||||
|
current: list[str] = []
|
||||||
|
brace_depth = 0
|
||||||
|
in_quote = False
|
||||||
|
escaped = False
|
||||||
|
ended_with_separator = False
|
||||||
|
|
||||||
|
for char in segment:
|
||||||
|
if escaped:
|
||||||
|
current.append(char)
|
||||||
|
escaped = False
|
||||||
|
ended_with_separator = False
|
||||||
|
continue
|
||||||
|
if char == "\\":
|
||||||
|
current.append(char)
|
||||||
|
escaped = True
|
||||||
|
ended_with_separator = False
|
||||||
|
continue
|
||||||
|
if char == '"' and brace_depth == 0:
|
||||||
|
in_quote = not in_quote
|
||||||
|
ended_with_separator = False
|
||||||
|
continue
|
||||||
|
if not in_quote and char == "{":
|
||||||
|
brace_depth += 1
|
||||||
|
ended_with_separator = False
|
||||||
|
continue
|
||||||
|
if not in_quote and char == "}" and brace_depth:
|
||||||
|
brace_depth -= 1
|
||||||
|
ended_with_separator = False
|
||||||
|
continue
|
||||||
|
if char.isspace() and not in_quote and brace_depth == 0:
|
||||||
|
if current:
|
||||||
|
words.append("".join(current))
|
||||||
|
current = []
|
||||||
|
ended_with_separator = bool(words)
|
||||||
|
continue
|
||||||
|
|
||||||
|
current.append(char)
|
||||||
|
ended_with_separator = False
|
||||||
|
|
||||||
|
if current:
|
||||||
|
words.append("".join(current))
|
||||||
|
elif ended_with_separator:
|
||||||
|
words.append("")
|
||||||
|
return words
|
||||||
|
|
||||||
|
|
||||||
|
def _codepoint_offset(text: str, utf16_offset: int) -> int | None:
|
||||||
|
if utf16_offset < 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
consumed = 0
|
||||||
|
for index, char in enumerate(text):
|
||||||
|
if consumed == utf16_offset:
|
||||||
|
return index
|
||||||
|
consumed += 2 if ord(char) > 0xFFFF else 1
|
||||||
|
if consumed > utf16_offset:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if consumed == utf16_offset:
|
||||||
|
return len(text)
|
||||||
|
return None
|
||||||
@@ -6,18 +6,20 @@ SRC_DIR = THIS_DIR.parent.parent / "src"
|
|||||||
if str(SRC_DIR) not in sys.path:
|
if str(SRC_DIR) not in sys.path:
|
||||||
sys.path.insert(0, str(SRC_DIR))
|
sys.path.insert(0, str(SRC_DIR))
|
||||||
|
|
||||||
import lsp_server
|
|
||||||
import lsprotocol.types as lsp # type: ignore
|
import lsprotocol.types as lsp # type: ignore
|
||||||
from common.load_data import standard_items
|
|
||||||
from lsp_tclserver import TclLanguageServer
|
|
||||||
from pygls.workspace import Workspace
|
from pygls.workspace import Workspace
|
||||||
from pygls.workspace.text_document import TextDocument
|
from pygls.workspace.text_document import TextDocument
|
||||||
|
|
||||||
|
import lsp_server
|
||||||
|
from common.load_data import standard_items
|
||||||
|
from lsp_tclserver import TclLanguageServer
|
||||||
from tools.completion_items import (
|
from tools.completion_items import (
|
||||||
COMMAND_KINDS,
|
COMMAND_KINDS,
|
||||||
VARIABLE_KINDS,
|
VARIABLE_KINDS,
|
||||||
CompletionContext,
|
CompletionContext,
|
||||||
completion_context,
|
completion_context,
|
||||||
)
|
)
|
||||||
|
from tools.tcl_command_completion import tcl_argument_completions
|
||||||
|
|
||||||
|
|
||||||
def _position_after(source: str, token: str, occurrence: int = 0) -> lsp.Position:
|
def _position_after(source: str, token: str, occurrence: int = 0) -> lsp.Position:
|
||||||
@@ -90,6 +92,18 @@ def _complete(document: TextDocument, position: lsp.Position):
|
|||||||
).items
|
).items
|
||||||
|
|
||||||
|
|
||||||
|
def _argument_completion_labels(source: str) -> set[str] | None:
|
||||||
|
lines = source.split("\n")
|
||||||
|
character = len(lines[-1].encode("utf-16-le")) // 2
|
||||||
|
items = tcl_argument_completions(
|
||||||
|
lines,
|
||||||
|
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):
|
||||||
_, current, source = _completion_server(tmp_path, monkeypatch)
|
_, current, source = _completion_server(tmp_path, monkeypatch)
|
||||||
items = _complete(current, _position_after(source, "$local"))
|
items = _complete(current, _position_after(source, "$local"))
|
||||||
@@ -150,3 +164,147 @@ def test_completion_context_handles_nested_commands_and_utf16():
|
|||||||
completion_context(["puts value"], lsp.Position(line=0, character=10))
|
completion_context(["puts value"], lsp.Position(line=0, character=10))
|
||||||
== CompletionContext.GENERAL
|
== CompletionContext.GENERAL
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_string_subcommands_and_compare_options_are_context_aware():
|
||||||
|
subcommands = _argument_completion_labels("string ")
|
||||||
|
assert subcommands is not None
|
||||||
|
assert {"compare", "equal", "is", "map", "match"} <= subcommands
|
||||||
|
|
||||||
|
assert _argument_completion_labels("string compare ") == {
|
||||||
|
"-length",
|
||||||
|
"-nocase",
|
||||||
|
}
|
||||||
|
assert _argument_completion_labels("string compare -nocase ") == {
|
||||||
|
"-length"
|
||||||
|
}
|
||||||
|
assert _argument_completion_labels("string compare -length ") is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_string_completion_handles_nested_commands_and_is_values():
|
||||||
|
assert _argument_completion_labels("set result [string compare -") == {
|
||||||
|
"-length",
|
||||||
|
"-nocase",
|
||||||
|
}
|
||||||
|
|
||||||
|
classes = _argument_completion_labels("string is ")
|
||||||
|
assert classes is not None
|
||||||
|
assert {"boolean", "double", "integer", "wideinteger"} <= classes
|
||||||
|
assert _argument_completion_labels("string is integer ") == {
|
||||||
|
"-failindex",
|
||||||
|
"-strict",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_dict_array_namespace_file_and_info_subcommands():
|
||||||
|
dict_items = _argument_completion_labels("dict ")
|
||||||
|
assert dict_items is not None
|
||||||
|
assert {"create", "filter", "get", "set", "with"} <= dict_items
|
||||||
|
assert _argument_completion_labels("dict filter ") == {
|
||||||
|
"key",
|
||||||
|
"script",
|
||||||
|
"value",
|
||||||
|
}
|
||||||
|
|
||||||
|
assert _argument_completion_labels("array names values ") == {
|
||||||
|
"-exact",
|
||||||
|
"-glob",
|
||||||
|
"-regexp",
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace_items = _argument_completion_labels("namespace ")
|
||||||
|
assert namespace_items is not None
|
||||||
|
assert {"children", "ensemble", "eval", "which"} <= namespace_items
|
||||||
|
assert _argument_completion_labels("namespace which ") == {
|
||||||
|
"-command",
|
||||||
|
"-variable",
|
||||||
|
}
|
||||||
|
assert _argument_completion_labels("namespace ensemble ") == {
|
||||||
|
"configure",
|
||||||
|
"create",
|
||||||
|
"exists",
|
||||||
|
}
|
||||||
|
|
||||||
|
file_items = _argument_completion_labels("file ")
|
||||||
|
assert file_items is not None
|
||||||
|
assert {"copy", "delete", "exists", "normalize", "rename"} <= file_items
|
||||||
|
assert _argument_completion_labels("file copy ") == {"--", "-force"}
|
||||||
|
|
||||||
|
info_items = _argument_completion_labels("info ")
|
||||||
|
assert info_items is not None
|
||||||
|
assert {"args", "body", "commands", "exists", "procs", "vars"} <= info_items
|
||||||
|
|
||||||
|
|
||||||
|
def test_variable_context_still_takes_priority_inside_tcl_command(
|
||||||
|
tmp_path: Path, monkeypatch
|
||||||
|
):
|
||||||
|
_, current, source = _completion_server(tmp_path, monkeypatch)
|
||||||
|
command_source = source.replace(
|
||||||
|
" puts $local\n",
|
||||||
|
" puts $local\n string compare $localValue other\n",
|
||||||
|
)
|
||||||
|
current = _document(tmp_path / "current.tcl", command_source)
|
||||||
|
lsp_server.LSP_SERVER.workspace.put_text_document(
|
||||||
|
lsp.TextDocumentItem(
|
||||||
|
uri=current.uri,
|
||||||
|
language_id="tcl",
|
||||||
|
version=2,
|
||||||
|
text=command_source,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
items = _complete(current, _position_after(command_source, "$localValue"))
|
||||||
|
|
||||||
|
assert items
|
||||||
|
assert all(item.kind in VARIABLE_KINDS for item in items)
|
||||||
|
assert "localValue" in {item.label for item in items}
|
||||||
|
|
||||||
|
|
||||||
|
def test_lsp_completion_returns_only_matching_command_options(
|
||||||
|
tmp_path: Path, monkeypatch
|
||||||
|
):
|
||||||
|
_, current, _ = _completion_server(tmp_path, monkeypatch)
|
||||||
|
source = "string compare "
|
||||||
|
current = _document(tmp_path / "current.tcl", source)
|
||||||
|
lsp_server.LSP_SERVER.workspace.put_text_document(
|
||||||
|
lsp.TextDocumentItem(
|
||||||
|
uri=current.uri,
|
||||||
|
language_id="tcl",
|
||||||
|
version=2,
|
||||||
|
text=source,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
items = _complete(current, _position_after(source, source))
|
||||||
|
|
||||||
|
assert {item.label for item in items} == {"-length", "-nocase"}
|
||||||
|
assert all(item.kind == lsp.CompletionItemKind.Keyword for item in items)
|
||||||
|
assert all(item.sort_text.startswith("000:") for item in items)
|
||||||
|
|
||||||
|
|
||||||
|
def test_space_trigger_does_not_open_broad_fallback_completion(
|
||||||
|
tmp_path: Path, monkeypatch
|
||||||
|
):
|
||||||
|
_, current, _ = _completion_server(tmp_path, monkeypatch)
|
||||||
|
source = "set value "
|
||||||
|
current = _document(tmp_path / "current.tcl", source)
|
||||||
|
lsp_server.LSP_SERVER.workspace.put_text_document(
|
||||||
|
lsp.TextDocumentItem(
|
||||||
|
uri=current.uri,
|
||||||
|
language_id="tcl",
|
||||||
|
version=2,
|
||||||
|
text=source,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
result = lsp_server.on_completion(
|
||||||
|
lsp.CompletionParams(
|
||||||
|
text_document=lsp.TextDocumentIdentifier(uri=current.uri),
|
||||||
|
position=_position_after(source, source),
|
||||||
|
context=lsp.CompletionContext(
|
||||||
|
trigger_kind=lsp.CompletionTriggerKind.TriggerCharacter,
|
||||||
|
trigger_character=" ",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.items == []
|
||||||
|
|||||||
@@ -117,7 +117,6 @@ proc SERVICE_get_tool_data {} {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
LIB_GE_command_buffer_edit_replace MOM_end_of_program_LIB END_OF_PROGRAM @END_OF_PROG {
|
LIB_GE_command_buffer_edit_replace MOM_end_of_program_LIB END_OF_PROGRAM @END_OF_PROG {
|
||||||
MOM_do_template "end_of_program_rewind"
|
MOM_do_template "end_of_program_rewind"
|
||||||
} EndOfProgramRewind
|
} EndOfProgramRewind
|
||||||
|
|||||||
Reference in New Issue
Block a user