feat(server): index PSC scripts and provide cross-file TclOO navigation/completions
Add PSC (.psc) indexing and share TclOO class metadata across files so class definitions discovered via PSC layers can be used for completions, signature help, inlay hints, and "go to definition". Key behavior changes: - Client file watcher now includes *.psc and .vscode launch paths/tests updated to use the postprocessor test folder; .gitignore updated to ignore that folder. - Server watches .psc changes and refreshes a PSC script index; new tools/tcloo_navigation.py exposes tcloo_definition used by the language server to resolve cross-file class/constructor/method definitions. - Language server uses class_snapshot(document.path) when producing TclOO completions, signature help, and inlay hints so resolved class metadata is available across files. Also includes related docs/changelog updates, minor code formatting cleanups, and added tests for PSC/TclOO behavior.
This commit is contained in:
@@ -284,6 +284,7 @@ def _options(*labels: str) -> tuple[OptionSpec, ...]:
|
||||
|
||||
|
||||
OPTIONS_BY_PATH: dict[tuple[str, ...], tuple[OptionSpec, ...]] = {
|
||||
("unset",): _options("nocomplain"),
|
||||
("binary", "decode", "base64"): (OptionSpec("-strict"),),
|
||||
("binary", "encode", "base64"): (
|
||||
OptionSpec("-maxlen", takes_value=True),
|
||||
@@ -437,109 +438,51 @@ _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(("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(("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(("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(("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", "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
|
||||
),
|
||||
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(("load",), frozenset({1}), DynamicCompletionKind.PATH, (".dll", ".so", ".dylib")),
|
||||
DynamicCompletionRule(("open",), frozenset({1}), DynamicCompletionKind.PATH),
|
||||
DynamicCompletionRule(
|
||||
("source",), frozenset({1, 3}), DynamicCompletionKind.PATH, (".tcl",)
|
||||
),
|
||||
DynamicCompletionRule(("source",), frozenset({1, 3}), DynamicCompletionKind.PATH, (".tcl",)),
|
||||
*(
|
||||
DynamicCompletionRule(
|
||||
("file", subcommand),
|
||||
@@ -666,12 +609,7 @@ SUBCOMMAND_SNIPPET_ITEMS = {
|
||||
|
||||
|
||||
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}
|
||||
)
|
||||
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(
|
||||
@@ -688,9 +626,7 @@ TCL_COMMAND_ITEMS = tuple(
|
||||
)
|
||||
|
||||
|
||||
def line_prefix_at_position(
|
||||
source_lines: Sequence[str], position: Position
|
||||
) -> str | None:
|
||||
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):
|
||||
@@ -703,9 +639,7 @@ def line_prefix_at_position(
|
||||
return line[:codepoint_offset]
|
||||
|
||||
|
||||
def tcl_argument_completion(
|
||||
source_lines: Sequence[str], position: Position
|
||||
) -> TclArgumentCompletion | None:
|
||||
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
|
||||
@@ -771,7 +705,7 @@ def tcl_argument_completion(
|
||||
active_prefix,
|
||||
)
|
||||
if option_completion is not None:
|
||||
if active_prefix.startswith("-"):
|
||||
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),
|
||||
@@ -780,9 +714,7 @@ def tcl_argument_completion(
|
||||
)
|
||||
|
||||
if argument_items:
|
||||
return _merge_dynamic_completion(
|
||||
argument_items, dynamic_completion, active_prefix
|
||||
)
|
||||
return _merge_dynamic_completion(argument_items, dynamic_completion, active_prefix)
|
||||
return dynamic_completion
|
||||
|
||||
|
||||
@@ -827,9 +759,7 @@ def _option_completion(
|
||||
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
|
||||
)
|
||||
remaining_options = tuple(option.label for option in options if option.label not in used_options)
|
||||
return TclArgumentCompletion(
|
||||
items=_completion_items(
|
||||
remaining_options,
|
||||
@@ -840,16 +770,9 @@ def _option_completion(
|
||||
)
|
||||
|
||||
|
||||
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
|
||||
):
|
||||
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,
|
||||
@@ -873,9 +796,7 @@ def _merge_dynamic_completion(
|
||||
)
|
||||
|
||||
|
||||
def _completion_items(
|
||||
labels: Sequence[str], kind: CompletionItemKind, detail: str
|
||||
) -> tuple[CompletionItem, ...]:
|
||||
def _completion_items(labels: Sequence[str], kind: CompletionItemKind, detail: str) -> tuple[CompletionItem, ...]:
|
||||
return tuple(
|
||||
CompletionItem(
|
||||
label=label,
|
||||
@@ -913,9 +834,7 @@ def path_completion_items(
|
||||
except (OSError, ValueError):
|
||||
return ()
|
||||
|
||||
allowed_extensions = {
|
||||
extension.casefold() for extension in completion.path_extensions
|
||||
}
|
||||
allowed_extensions = {extension.casefold() for extension in completion.path_extensions}
|
||||
replace_start = max(
|
||||
0,
|
||||
position.character - len(raw_prefix.encode("utf-16-le")) // 2,
|
||||
@@ -933,28 +852,17 @@ def path_completion_items(
|
||||
is_directory = entry.is_dir()
|
||||
except OSError:
|
||||
continue
|
||||
if (
|
||||
not is_directory
|
||||
and allowed_extensions
|
||||
and entry.suffix.casefold() not in allowed_extensions
|
||||
):
|
||||
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
|
||||
)
|
||||
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
|
||||
),
|
||||
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),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user