Files
nx_post_support/server/src/tools/tcl_command_completion.py
T

1012 lines
31 KiB
Python

"""Command-aware completion data and cursor parsing for Tcl commands."""
from __future__ import annotations
from collections.abc import Sequence
from dataclasses import dataclass
from enum import Enum
from pathlib import Path
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"
BLOCK_TEMPLATE = "block_template"
ADDRESS = "address"
# A variable substituted as a value, inserted with a leading "$".
VALUE = "value"
@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)
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, ...]] = {
("unset",): _options("nocomplain"),
("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,
values=("ok", "error", "return", "break", "continue"),
),
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"),
(("MOM_force",), 1): ("Always", "Once", "Off"),
(("MOM_suppress",), 1): ("Always", "Once", "Off"),
(("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,
}
_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(("MOM_do_template",), frozenset({1}), DynamicCompletionKind.BLOCK_TEMPLATE),
DynamicCompletionRule(("MOM_ask_address_value",), frozenset({1}), DynamicCompletionKind.ADDRESS),
DynamicCompletionRule(("MOM_force",), frozenset({1}), DynamicCompletionKind.VALUE),
DynamicCompletionRule(("MOM_force",), _REPEATED_SUBCOMMAND_ARGUMENTS, DynamicCompletionKind.ADDRESS),
DynamicCompletionRule(("MOM_suppress",), frozenset({1}), DynamicCompletionKind.VALUE),
DynamicCompletionRule(("MOM_suppress",), _REPEATED_SUBCOMMAND_ARGUMENTS, DynamicCompletionKind.ADDRESS),
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(
sorted({path[0] for path in SUBCOMMANDS_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_SNIPPET_ITEMS.get(
command,
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_completion(source_lines: Sequence[str], position: Position) -> TclArgumentCompletion | None:
"""Describe static and dynamic argument completion 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])
dynamic_completion = _dynamic_completion(words, active_index, active_prefix)
subcommands = SUBCOMMANDS_BY_PATH.get(completed_path)
if subcommands is not None:
items = tuple(
SUBCOMMAND_SNIPPET_ITEMS.get(
(*completed_path, label),
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():
if active_index == argument_index and tuple(words[: len(path)]) == path:
return _merge_dynamic_completion(
_completion_items(
values,
CompletionItemKind.Value,
f"{' '.join(path)} value",
),
dynamic_completion,
active_prefix,
)
for path in sorted(OPTIONS_BY_PATH, key=len, reverse=True):
if active_index < len(path) or tuple(words[: len(path)]) != path:
continue
option_completion = _option_completion(
path,
OPTIONS_BY_PATH[path],
words[len(path) : active_index],
active_prefix,
)
if option_completion is not None:
if active_prefix.startswith("-") or (path == ("unset",) and active_index == 1 and not active_prefix):
return option_completion
return _merge_dynamic_completion(
(*argument_items, *option_completion.items),
dynamic_completion,
active_prefix,
)
if argument_items:
return _merge_dynamic_completion(argument_items, dynamic_completion, active_prefix)
return dynamic_completion
def _option_completion(
path: tuple[str, ...],
options: tuple[OptionSpec, ...],
completed_arguments: Sequence[str],
active_prefix: str,
) -> TclArgumentCompletion | 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 TclArgumentCompletion(
items=_completion_items(
option.values,
CompletionItemKind.Value,
f"{option.label} value",
),
active_prefix=active_prefix,
)
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 TclArgumentCompletion(
items=_completion_items(
remaining_options,
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(labels: Sequence[str], kind: CompletionItemKind, detail: str) -> tuple[CompletionItem, ...]:
return tuple(
CompletionItem(
label=label,
kind=kind,
detail=detail,
insert_text=label,
)
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:
"""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)
start = max(starts)
if brace_starts and start == brace_starts[-1] + 1:
# An unfinished braced body or expression can itself contain commands
# (for example, ``if {[string compare ...``). Scan that inner context
# independently, while keeping completed braced arguments opaque.
return _current_command_segment(line_prefix[start:])
return line_prefix[start:]
def _tokenize_command_segment(segment: str) -> list[str]:
words: list[str] = []
current: list[str] = []
brace_depth = 0
in_quote = False
escaped = False
ended_with_separator = False
word_started = False
for char in segment:
if escaped:
current.append(char)
escaped = False
ended_with_separator = False
word_started = True
continue
if char == "\\":
current.append(char)
escaped = True
ended_with_separator = False
word_started = True
continue
if char == '"' and brace_depth == 0:
in_quote = not in_quote
ended_with_separator = False
word_started = True
continue
if not in_quote and char == "{":
brace_depth += 1
ended_with_separator = False
word_started = True
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 or word_started:
words.append("".join(current))
current = []
word_started = False
ended_with_separator = bool(words)
continue
current.append(char)
ended_with_separator = False
word_started = True
if current or word_started:
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