Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91b86f29cb | ||
|
|
f88a50d4ab | ||
|
|
c28836933c | ||
|
|
91c8c4aff3 | ||
|
|
d8611d7aea | ||
|
|
7e9a4359cd | ||
|
|
b7f28ab1a3 | ||
|
|
9b368ba761 | ||
|
|
82e13cf7ce | ||
|
|
d104906508 | ||
|
|
a5ff3b55ff | ||
|
|
26225eb8ed | ||
|
|
844138b383 | ||
|
|
d7eb72f417 | ||
|
|
af5acfc946 | ||
|
|
20f76b6a20 | ||
|
|
081b488fe3 |
Vendored
+26
@@ -17,6 +17,19 @@
|
|||||||
],
|
],
|
||||||
"cwd": "${env:TEMP}/nx-post-support-vscode-debug",
|
"cwd": "${env:TEMP}/nx-post-support-vscode-debug",
|
||||||
"outFiles": ["${env:TEMP}/nx-post-support-vscode-debug/dist/**/*.js"],
|
"outFiles": ["${env:TEMP}/nx-post-support-vscode-debug/dist/**/*.js"],
|
||||||
|
"linux": {
|
||||||
|
"args": [
|
||||||
|
"--extensionDevelopmentPath=${workspaceFolder}",
|
||||||
|
"${workspaceFolder}",
|
||||||
|
"${workspaceFolder}/test/test.tcl"
|
||||||
|
],
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
|
||||||
|
"resolveSourceMapLocations": [
|
||||||
|
"${workspaceFolder}/dist/**/*.js",
|
||||||
|
"!**/node_modules/**"
|
||||||
|
]
|
||||||
|
},
|
||||||
"sourceMaps": true,
|
"sourceMaps": true,
|
||||||
"resolveSourceMapLocations": [
|
"resolveSourceMapLocations": [
|
||||||
"${env:TEMP}/nx-post-support-vscode-debug/dist/**/*.js",
|
"${env:TEMP}/nx-post-support-vscode-debug/dist/**/*.js",
|
||||||
@@ -50,6 +63,19 @@
|
|||||||
],
|
],
|
||||||
"cwd": "${env:TEMP}/nx-post-support-vscode-debug",
|
"cwd": "${env:TEMP}/nx-post-support-vscode-debug",
|
||||||
"outFiles": ["${env:TEMP}/nx-post-support-vscode-debug/dist/**/*.js"],
|
"outFiles": ["${env:TEMP}/nx-post-support-vscode-debug/dist/**/*.js"],
|
||||||
|
"linux": {
|
||||||
|
"args": [
|
||||||
|
"--extensionDevelopmentPath=${workspaceFolder}",
|
||||||
|
"${workspaceFolder}",
|
||||||
|
"${workspaceFolder}/test/test.tcl"
|
||||||
|
],
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
|
||||||
|
"resolveSourceMapLocations": [
|
||||||
|
"${workspaceFolder}/dist/**/*.js",
|
||||||
|
"!**/node_modules/**"
|
||||||
|
]
|
||||||
|
},
|
||||||
"sourceMaps": true,
|
"sourceMaps": true,
|
||||||
"resolveSourceMapLocations": [
|
"resolveSourceMapLocations": [
|
||||||
"${env:TEMP}/nx-post-support-vscode-debug/dist/**/*.js",
|
"${env:TEMP}/nx-post-support-vscode-debug/dist/**/*.js",
|
||||||
|
|||||||
Vendored
+7
@@ -15,6 +15,13 @@
|
|||||||
"${workspaceFolder}",
|
"${workspaceFolder}",
|
||||||
"${env:TEMP}\\nx-post-support-vscode-debug"
|
"${env:TEMP}\\nx-post-support-vscode-debug"
|
||||||
],
|
],
|
||||||
|
"linux": {
|
||||||
|
"command": "npm",
|
||||||
|
"args": ["run", "compile:debug"],
|
||||||
|
"options": {
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
}
|
||||||
|
},
|
||||||
"problemMatcher": [],
|
"problemMatcher": [],
|
||||||
"presentation": {
|
"presentation": {
|
||||||
"reveal": "always",
|
"reveal": "always",
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
- Add document-local TclOO method completion for `new`/`create` instances, `my`, and statically inferred return chains
|
||||||
|
- Suggest TclOO class names as classes and semantically highlight their declarations and calls
|
||||||
|
- Show signature help and parameter inlay hints for resolved TclOO methods and constructors, including optional and variadic arguments
|
||||||
- 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
|
||||||
|
- 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 or commands 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
|
- 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,7 +105,7 @@ export function createCdlEventHandlerSnippet(handler: CdlEventHandler): string {
|
|||||||
const globals = [
|
const globals = [
|
||||||
...new Set(handler.parameterNames.map((parameter) => momVariableName(parameter)))
|
...new Set(handler.parameterNames.map((parameter) => momVariableName(parameter)))
|
||||||
]
|
]
|
||||||
const lines = [`proc ${momEventName(handler.eventName)} { } {`]
|
const lines = [`proc ${momEventName(handler.eventName)} {args} {`]
|
||||||
|
|
||||||
if (globals.length > 0) {
|
if (globals.length > 0) {
|
||||||
lines.push(...globals.map((variable) => ` global ${variable}`), "")
|
lines.push(...globals.map((variable) => ` global ${variable}`), "")
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
"name": "nx-post-support",
|
"name": "nx-post-support",
|
||||||
"displayName": "NX Postprocessor Support",
|
"displayName": "NX Postprocessor Support",
|
||||||
"description": "VS Code extension for NX CAM postprocessor development with language support and remote Tcl debugging for CDL, TCL, and DEF files",
|
"description": "VS Code extension for NX CAM postprocessor development with language support and remote Tcl debugging for CDL, TCL, and DEF files",
|
||||||
"version": "2026.8.300",
|
"version": "2026.9.300",
|
||||||
"publisher": "Christoph",
|
"publisher": "Christoph",
|
||||||
"icon": "images/nx-1.png",
|
"icon": "images/nx-1.png",
|
||||||
"activationEvents": [
|
"activationEvents": [
|
||||||
|
|||||||
+172
-9
@@ -38,13 +38,19 @@ 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,
|
||||||
|
array_element_completions,
|
||||||
|
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 +75,17 @@ 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.tcloo_arguments import method_signature_help
|
||||||
|
from tclint.lexer import TclSyntaxError
|
||||||
|
from tools.tcloo_completion import parse_completion_source, tcloo_completions
|
||||||
|
from tools.tcloo_symbols import class_completion_items
|
||||||
|
from tools.tcl_command_completion import (
|
||||||
|
TCL_COMMAND_ITEMS,
|
||||||
|
TCL_COMMAND_NAMES,
|
||||||
|
DynamicCompletionKind,
|
||||||
|
path_completion_items,
|
||||||
|
tcl_argument_completion,
|
||||||
|
)
|
||||||
|
|
||||||
WORKSPACE_SETTINGS = {}
|
WORKSPACE_SETTINGS = {}
|
||||||
GLOBAL_SETTINGS = {}
|
GLOBAL_SETTINGS = {}
|
||||||
@@ -82,10 +99,19 @@ 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}
|
||||||
|
_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_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]
|
||||||
+ standard_items.nx_procs
|
+ standard_items.nx_procs
|
||||||
+ standard_items.nx_variables
|
+ standard_items.nx_variables
|
||||||
)
|
)
|
||||||
@@ -250,16 +276,81 @@ 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)
|
||||||
|
oo_items = tcloo_completions(source_lines, position)
|
||||||
|
if oo_items is not None:
|
||||||
|
return lsp.CompletionList(is_incomplete=False, items=oo_items)
|
||||||
|
array_items = array_element_completions(
|
||||||
|
source_lines, position, LSP_SERVER.navigation_snapshot().values(),
|
||||||
|
str(pathlib.Path(uris.to_fs_path(doc.uri))),
|
||||||
|
)
|
||||||
|
if array_items is not None:
|
||||||
|
return lsp.CompletionList(is_incomplete=False, items=array_items)
|
||||||
|
context = completion_context(source_lines, position)
|
||||||
|
|
||||||
|
# Variable completion wins inside command arguments. Otherwise prefer the
|
||||||
|
# narrow command grammar when the cursor is at a known subcommand/option.
|
||||||
|
argument_completion = None
|
||||||
|
if context != CompletionContext.VARIABLE:
|
||||||
|
argument_completion = tcl_argument_completion(source_lines, position)
|
||||||
|
if (
|
||||||
|
argument_completion is not None
|
||||||
|
and argument_completion.dynamic_kind is None
|
||||||
|
):
|
||||||
|
items = ranked_completion_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,
|
||||||
|
)
|
||||||
|
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 (
|
||||||
|
argument_completion is None
|
||||||
|
and 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=[])
|
||||||
|
|
||||||
|
try:
|
||||||
tree = LSP_SERVER.get_tree(doc)
|
tree = LSP_SERVER.get_tree(doc)
|
||||||
|
except TclSyntaxError:
|
||||||
|
tree = parse_completion_source(doc.source)
|
||||||
|
if tree is None:
|
||||||
|
return lsp.CompletionList(is_incomplete=False, items=[])
|
||||||
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
|
||||||
@@ -268,6 +359,7 @@ def on_completion(params: lsp.CompletionParams) -> lsp.CompletionList:
|
|||||||
break
|
break
|
||||||
|
|
||||||
candidates: list[tuple[int, lsp.CompletionItem]] = []
|
candidates: list[tuple[int, lsp.CompletionItem]] = []
|
||||||
|
candidates.extend((0, item) for item in class_completion_items(tree))
|
||||||
for name in sorted(local_names - globals_set):
|
for name in sorted(local_names - globals_set):
|
||||||
candidates.append(
|
candidates.append(
|
||||||
(
|
(
|
||||||
@@ -301,8 +393,70 @@ 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)
|
||||||
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)
|
||||||
|
|
||||||
@@ -316,7 +470,15 @@ def on_completion(params: lsp.CompletionParams) -> lsp.CompletionList:
|
|||||||
)
|
)
|
||||||
def signature_help(params: lsp.SignatureHelpParams) -> lsp.SignatureHelp | None:
|
def signature_help(params: lsp.SignatureHelpParams) -> lsp.SignatureHelp | None:
|
||||||
document = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
|
document = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
|
||||||
|
method_help = method_signature_help(document.source, params.position)
|
||||||
|
if method_help is not None:
|
||||||
|
return method_help
|
||||||
|
try:
|
||||||
tree = LSP_SERVER.get_tree(document)
|
tree = LSP_SERVER.get_tree(document)
|
||||||
|
except TclSyntaxError:
|
||||||
|
tree = parse_completion_source(document.source)
|
||||||
|
if tree is None:
|
||||||
|
return None
|
||||||
|
|
||||||
custom_signatures, custom_docs = LSP_SERVER.proc_metadata_snapshot(
|
custom_signatures, custom_docs = LSP_SERVER.proc_metadata_snapshot(
|
||||||
document.path
|
document.path
|
||||||
@@ -400,6 +562,7 @@ def semantic_tokens(params: lsp.SemanticTokensParams):
|
|||||||
|
|
||||||
# Reuse cached AST
|
# Reuse cached AST
|
||||||
tree = LSP_SERVER.get_tree(document)
|
tree = LSP_SERVER.get_tree(document)
|
||||||
|
hl.highlight_classes(tree)
|
||||||
tree.accept(hl, recurse=True)
|
tree.accept(hl, recurse=True)
|
||||||
|
|
||||||
tokens = hl.tokens()
|
tokens = hl.tokens()
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from tclint.lexer import TclSyntaxError
|
|||||||
from tclint.violations import Violation
|
from tclint.violations import Violation
|
||||||
from tools import checks, parser
|
from tools import checks, parser
|
||||||
from tools.completion_items import CompletionCollector
|
from tools.completion_items import CompletionCollector
|
||||||
|
from tools.tcloo_symbols import class_completion_items
|
||||||
from tools.formatter import NxFormatter as Formatter
|
from tools.formatter import NxFormatter as Formatter
|
||||||
from tools.inlay_hint import InlayHintSignature, build_custom_inlay_signatures
|
from tools.inlay_hint import InlayHintSignature, build_custom_inlay_signatures
|
||||||
from tools.navigation import FileSymbolIndex, build_file_symbol_index
|
from tools.navigation import FileSymbolIndex, build_file_symbol_index
|
||||||
@@ -190,6 +191,7 @@ class TclLanguageServer(LanguageServer):
|
|||||||
item.label
|
item.label
|
||||||
for path_items in self.poco_completion.values()
|
for path_items in self.poco_completion.values()
|
||||||
for item in path_items
|
for item in path_items
|
||||||
|
if item.kind != lsp.CompletionItemKind.Class
|
||||||
)
|
)
|
||||||
self._custom_function_names_cache = (self._index_generation, names)
|
self._custom_function_names_cache = (self._index_generation, names)
|
||||||
return names
|
return names
|
||||||
@@ -542,6 +544,7 @@ class TclLanguageServer(LanguageServer):
|
|||||||
else self.parse_source(document.source)
|
else self.parse_source(document.source)
|
||||||
)
|
)
|
||||||
tree.accept(collector, recurse=True)
|
tree.accept(collector, recurse=True)
|
||||||
|
collector.custom_functions.extend(class_completion_items(tree))
|
||||||
docs = build_proc_docs(tree, document.source)
|
docs = build_proc_docs(tree, document.source)
|
||||||
navigation_index = build_file_symbol_index(
|
navigation_index = build_file_symbol_index(
|
||||||
filepath, document.uri, tree
|
filepath, document.uri, tree
|
||||||
|
|||||||
@@ -6,9 +6,17 @@ 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.navigation import FileSymbolIndex
|
||||||
|
from tools.variable_names import variable_name
|
||||||
|
from tools.tcl_command_completion import (
|
||||||
|
DynamicCompletionKind,
|
||||||
|
line_prefix_at_position,
|
||||||
|
tcl_argument_completion,
|
||||||
|
)
|
||||||
|
|
||||||
BUILTIN_VAR_LABELS = {ci.label for ci in standard_items.nx_variables}
|
BUILTIN_VAR_LABELS = {ci.label for ci in standard_items.nx_variables}
|
||||||
BUILTIN_PROC_LABELS = {ci.label for ci in standard_items.nx_procs}
|
BUILTIN_PROC_LABELS = {ci.label for ci in standard_items.nx_procs}
|
||||||
|
|
||||||
@@ -25,37 +33,112 @@ VARIABLE_KINDS = {
|
|||||||
lsp.CompletionItemKind.Constant,
|
lsp.CompletionItemKind.Constant,
|
||||||
}
|
}
|
||||||
COMMAND_KINDS = {
|
COMMAND_KINDS = {
|
||||||
|
lsp.CompletionItemKind.Class,
|
||||||
lsp.CompletionItemKind.Function,
|
lsp.CompletionItemKind.Function,
|
||||||
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_:]*$")
|
||||||
_COMMAND_PREFIX_RE = re.compile(r"(?:^|[;\[\{])\s*[^\s;\[\]\{\}]*$")
|
_COMMAND_PREFIX_RE = re.compile(r"(?:^|[;\[\{])\s*[^\s;\[\]\{\}]*$")
|
||||||
|
_ARRAY_PREFIX_RE = re.compile(
|
||||||
|
r"(?P<name>(?:::)?[A-Za-z_][A-Za-z0-9_:]*)\((?P<key>[^()\n]*)$"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _codepoint_offset(line: str, utf16_offset: int) -> int:
|
def array_element_completions(
|
||||||
"""Translate an LSP UTF-16 character offset into a Python string offset."""
|
source_lines: Sequence[str],
|
||||||
if utf16_offset <= 0:
|
position: lsp.Position,
|
||||||
return 0
|
indexes: Iterable[FileSymbolIndex],
|
||||||
|
current_path: str,
|
||||||
units = 0
|
) -> list[lsp.CompletionItem] | None:
|
||||||
for offset, character in enumerate(line):
|
"""Complete literal array keys, replacing only the text inside parentheses."""
|
||||||
units += 2 if ord(character) > 0xFFFF else 1
|
prefix = line_prefix_at_position(source_lines, position)
|
||||||
if units >= utf16_offset:
|
if prefix is None:
|
||||||
return offset + 1
|
return None
|
||||||
return len(line)
|
match = _ARRAY_PREFIX_RE.search(prefix)
|
||||||
|
if match is None:
|
||||||
|
return None
|
||||||
|
before = prefix[:match.start()]
|
||||||
|
if not before.endswith("$"):
|
||||||
|
argument = tcl_argument_completion(source_lines, position)
|
||||||
|
if argument is None or argument.dynamic_kind != DynamicCompletionKind.VARIABLE:
|
||||||
|
return None
|
||||||
|
name, key_prefix = match.group("name", "key")
|
||||||
|
part_index = key_prefix.count(",")
|
||||||
|
key_prefix = key_prefix.rsplit(",", 1)[-1]
|
||||||
|
if any(char in key_prefix for char in "$[]{}\\"):
|
||||||
|
return None
|
||||||
|
indexes = list(indexes)
|
||||||
|
local_scope = None
|
||||||
|
for index in indexes:
|
||||||
|
if index.path != current_path:
|
||||||
|
continue
|
||||||
|
for occurrence in index.occurrences:
|
||||||
|
span = occurrence.declaration_range
|
||||||
|
if (
|
||||||
|
occurrence.identity.kind == "proc"
|
||||||
|
and occurrence.is_definition
|
||||||
|
and span is not None
|
||||||
|
and span.start.line <= position.line <= span.end.line
|
||||||
|
):
|
||||||
|
local_scope = f"{index.path}::proc::{occurrence.identity.name}"
|
||||||
|
keys: dict[str, set[tuple[str | None, ...]]] = {}
|
||||||
|
for index in indexes:
|
||||||
|
for occurrence in index.occurrences:
|
||||||
|
parts = occurrence.array_parts
|
||||||
|
key = parts[part_index] if part_index < len(parts) else None
|
||||||
|
if (
|
||||||
|
key
|
||||||
|
and (
|
||||||
|
occurrence.identity.scope is None
|
||||||
|
or occurrence.identity.scope == local_scope
|
||||||
|
)
|
||||||
|
and key.startswith(key_prefix)
|
||||||
|
and not any(char in key for char in "$[]\\")
|
||||||
|
and occurrence.identity.name.removeprefix("::") == name.removeprefix("::")
|
||||||
|
):
|
||||||
|
keys.setdefault(key, set()).add(occurrence.array_template_parts[part_index + 1:])
|
||||||
|
line = source_lines[position.line]
|
||||||
|
suffix = line[len(prefix):]
|
||||||
|
remaining = re.match(r"[^(),\s$\[\]{}]*", suffix).group()
|
||||||
|
has_close = suffix[len(remaining):].startswith((")", ","))
|
||||||
|
start = position.character - len(key_prefix.encode("utf-16-le")) // 2
|
||||||
|
end = position.character + len(remaining.encode("utf-16-le")) // 2
|
||||||
|
items = []
|
||||||
|
for key in sorted(keys):
|
||||||
|
new_text = key
|
||||||
|
# Fill missing index components only; keep an existing comma and suffix.
|
||||||
|
if not suffix[len(remaining):].startswith(","):
|
||||||
|
tails = sorted(keys[key], key=lambda tail: (len(tail), repr(tail)))
|
||||||
|
tail = tails[0]
|
||||||
|
if all(part is not None for part in tail) and any("$" in part for part in tail):
|
||||||
|
new_text = ",".join([key, *tail])
|
||||||
|
new_text += "" if has_close else ")"
|
||||||
|
items.append(lsp.CompletionItem(
|
||||||
|
label=key,
|
||||||
|
kind=lsp.CompletionItemKind.Field,
|
||||||
|
detail=f"{name}({key})",
|
||||||
|
insert_text_format=lsp.InsertTextFormat.PlainText,
|
||||||
|
text_edit=lsp.TextEdit(
|
||||||
|
range=lsp.Range(
|
||||||
|
start=lsp.Position(line=position.line, character=start),
|
||||||
|
end=lsp.Position(line=position.line, character=end),
|
||||||
|
),
|
||||||
|
new_text=new_text,
|
||||||
|
),
|
||||||
|
))
|
||||||
|
return items
|
||||||
|
|
||||||
|
|
||||||
def completion_context(
|
def completion_context(
|
||||||
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):
|
||||||
@@ -179,8 +262,8 @@ class CompletionCollector(Visitor):
|
|||||||
# Collect variables set with explicit global namespace: set ::var_name ...
|
# Collect variables set with explicit global namespace: set ::var_name ...
|
||||||
elif routine.contents == "set" and command.args:
|
elif routine.contents == "set" and command.args:
|
||||||
first = command.args[0]
|
first = command.args[0]
|
||||||
if isinstance(first, BareWord) and getattr(first, "value", None):
|
var_name = variable_name(first)
|
||||||
var_name = first.value
|
if var_name:
|
||||||
if var_name.startswith("::"):
|
if var_name.startswith("::"):
|
||||||
base_name = var_name.split("(", 1)[0]
|
base_name = var_name.split("(", 1)[0]
|
||||||
clean_name = base_name[2:] # remove leading '::' for completion display
|
clean_name = base_name[2:] # remove leading '::' for completion display
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ from typing import Any
|
|||||||
import lsprotocol.types as lsp
|
import lsprotocol.types as lsp
|
||||||
from tclint.syntax_tree import Command, VarSub, Visitor
|
from tclint.syntax_tree import Command, VarSub, Visitor
|
||||||
from tools.navigation import FileSymbolIndex
|
from tools.navigation import FileSymbolIndex
|
||||||
|
from tools.tcloo_arguments import method_parameters
|
||||||
|
from tools.tcloo_completion import resolved_method_calls
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -160,6 +162,7 @@ class InlayHintGenerator(Visitor):
|
|||||||
self.source_lines = (
|
self.source_lines = (
|
||||||
source_lines if source_lines is not None else source.splitlines()
|
source_lines if source_lines is not None else source.splitlines()
|
||||||
)
|
)
|
||||||
|
self.source = source
|
||||||
self.proc_signatures = proc_signatures
|
self.proc_signatures = proc_signatures
|
||||||
self.requested_range = requested_range
|
self.requested_range = requested_range
|
||||||
self.parameter_names = parameter_names
|
self.parameter_names = parameter_names
|
||||||
@@ -194,6 +197,17 @@ class InlayHintGenerator(Visitor):
|
|||||||
walk(child)
|
walk(child)
|
||||||
|
|
||||||
walk(tree)
|
walk(tree)
|
||||||
|
if self.parameter_names != "none":
|
||||||
|
for call in resolved_method_calls(self.source):
|
||||||
|
if not self._node_intersects_requested_range(call.command):
|
||||||
|
continue
|
||||||
|
parameters = method_parameters(call.parameters)
|
||||||
|
signature = InlayHintSignature(
|
||||||
|
parameters=tuple(InlayHintParameter(p.name, variadic=p.variadic) for p in parameters),
|
||||||
|
display_label=" ".join([call.label, *(p.label for p in parameters)]),
|
||||||
|
)
|
||||||
|
self._argument_hints(signature, call.command.args[call.argument_offset:])
|
||||||
|
self.hints.sort(key=lambda hint: (hint.position.line, hint.position.character))
|
||||||
return self.hints
|
return self.hints
|
||||||
|
|
||||||
def _position(self, line: int, column: int) -> lsp.Position:
|
def _position(self, line: int, column: int) -> lsp.Position:
|
||||||
@@ -238,7 +252,10 @@ class InlayHintGenerator(Visitor):
|
|||||||
if signature is None or self.parameter_names == "none":
|
if signature is None or self.parameter_names == "none":
|
||||||
return
|
return
|
||||||
|
|
||||||
for argument_index, argument in enumerate(command.args):
|
self._argument_hints(signature, command.args)
|
||||||
|
|
||||||
|
def _argument_hints(self, signature, arguments):
|
||||||
|
for argument_index, argument in enumerate(arguments):
|
||||||
parameter = self._parameter_for_argument(signature, argument_index)
|
parameter = self._parameter_for_argument(signature, argument_index)
|
||||||
if parameter is None:
|
if parameter is None:
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ from dataclasses import dataclass
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import lsprotocol.types as lsp
|
import lsprotocol.types as lsp
|
||||||
from tclint.syntax_tree import Command, List, Node, Script, VarSub
|
from tclint.syntax_tree import Command, List, Node, QuotedWord, Script, VarSub
|
||||||
|
from tools.variable_names import array_key_parts, variable_name
|
||||||
|
|
||||||
ROOT_NAMESPACE = "::"
|
ROOT_NAMESPACE = "::"
|
||||||
|
|
||||||
@@ -27,6 +28,9 @@ class SymbolOccurrence:
|
|||||||
fallback_identity: SymbolIdentity | None = None
|
fallback_identity: SymbolIdentity | None = None
|
||||||
caller: SymbolIdentity | None = None
|
caller: SymbolIdentity | None = None
|
||||||
declaration_range: lsp.Range | None = None
|
declaration_range: lsp.Range | None = None
|
||||||
|
array_element: str | None = None
|
||||||
|
array_parts: tuple[str | None, ...] = ()
|
||||||
|
array_template_parts: tuple[str | None, ...] = ()
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -96,6 +100,8 @@ def _name_range(node: Node, raw_name: str, *, variable_sub: bool = False) -> lsp
|
|||||||
column += 2 if getattr(node, "braced", False) else 1
|
column += 2 if getattr(node, "braced", False) else 1
|
||||||
else:
|
else:
|
||||||
position = getattr(node, "contents_pos", None) or node.pos
|
position = getattr(node, "contents_pos", None) or node.pos
|
||||||
|
if isinstance(node, QuotedWord) and node.contents is None and node.children:
|
||||||
|
position = node.children[0].pos
|
||||||
line, column = position
|
line, column = position
|
||||||
|
|
||||||
normalized = _without_array_index(raw_name)
|
normalized = _without_array_index(raw_name)
|
||||||
@@ -312,6 +318,13 @@ def build_file_symbol_index(
|
|||||||
is_definition=is_definition,
|
is_definition=is_definition,
|
||||||
symbol_kind=lsp.SymbolKind.Variable,
|
symbol_kind=lsp.SymbolKind.Variable,
|
||||||
container_name=_container_name(symbol_identity),
|
container_name=_container_name(symbol_identity),
|
||||||
|
array_element=(
|
||||||
|
raw_name.split("(", 1)[1][:-1]
|
||||||
|
if "(" in raw_name and raw_name.endswith(")")
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
array_parts=array_key_parts(node),
|
||||||
|
array_template_parts=array_key_parts(node, preserve_variables=True),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -444,7 +457,7 @@ def build_file_symbol_index(
|
|||||||
for node, is_definition in _variable_command_nodes(command):
|
for node, is_definition in _variable_command_nodes(command):
|
||||||
if id(node) in declaration_ids:
|
if id(node) in declaration_ids:
|
||||||
continue
|
continue
|
||||||
raw_name = _static_contents(node)
|
raw_name = variable_name(node)
|
||||||
if raw_name:
|
if raw_name:
|
||||||
add_variable(
|
add_variable(
|
||||||
node,
|
node,
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import attrs
|
|||||||
from common.load_data import standard_items
|
from common.load_data import standard_items
|
||||||
from tclint.commands.plugins import PluginManager
|
from tclint.commands.plugins import PluginManager
|
||||||
from tclint.syntax_tree import BareWord, Command, QuotedWord, Visitor
|
from tclint.syntax_tree import BareWord, Command, QuotedWord, Visitor
|
||||||
|
from tools.variable_names import variable_name
|
||||||
|
from tools.tcloo_symbols import class_symbols
|
||||||
|
|
||||||
# Constructing a PluginManager scans entry points, and get_commands() rebuilds
|
# Constructing a PluginManager scans entry points, and get_commands() rebuilds
|
||||||
# the builtin command set on every call. Semantic tokens are requested often, so
|
# the builtin command set on every call. Semantic tokens are requested often, so
|
||||||
@@ -69,6 +71,7 @@ class _Highlighter(Visitor):
|
|||||||
def __init__(self, plugins, custom_functions):
|
def __init__(self, plugins, custom_functions):
|
||||||
self._commands = _load_commands(plugins)
|
self._commands = _load_commands(plugins)
|
||||||
self._tokens = []
|
self._tokens = []
|
||||||
|
self._class_tokens = {}
|
||||||
if isinstance(custom_functions, dict):
|
if isinstance(custom_functions, dict):
|
||||||
self._custom_function_names = frozenset(
|
self._custom_function_names = frozenset(
|
||||||
item.label
|
item.label
|
||||||
@@ -83,6 +86,17 @@ class _Highlighter(Visitor):
|
|||||||
return
|
return
|
||||||
self._tokens.append((position, length, tok_type, modifiers or []))
|
self._tokens.append((position, length, tok_type, modifiers or []))
|
||||||
|
|
||||||
|
def highlight_classes(self, tree):
|
||||||
|
declarations, references = class_symbols(tree)
|
||||||
|
for node, modifiers in [
|
||||||
|
*((node, [TokenModifier.declaration]) for node in declarations.values()),
|
||||||
|
*((node, []) for node in references),
|
||||||
|
]:
|
||||||
|
line, col = node.contents_pos
|
||||||
|
self._class_tokens[(line - 1, col - 1)] = (
|
||||||
|
(line - 1, col - 1), len(node.contents), "class", modifiers,
|
||||||
|
)
|
||||||
|
|
||||||
def _get_token_info(self, node):
|
def _get_token_info(self, node):
|
||||||
"""Hilfsmethode um Token-Informationen aus verschiedenen Node-Typen zu extrahieren."""
|
"""Hilfsmethode um Token-Informationen aus verschiedenen Node-Typen zu extrahieren."""
|
||||||
if not hasattr(node, "pos"):
|
if not hasattr(node, "pos"):
|
||||||
@@ -159,6 +173,11 @@ class _Highlighter(Visitor):
|
|||||||
if routine.contents == "set" and command.args:
|
if routine.contents == "set" and command.args:
|
||||||
first_arg = command.args[0]
|
first_arg = command.args[0]
|
||||||
token_info = self._get_token_info(first_arg)
|
token_info = self._get_token_info(first_arg)
|
||||||
|
if first_arg.contents is None:
|
||||||
|
name = variable_name(first_arg)
|
||||||
|
if name:
|
||||||
|
line, col = first_arg.children[0].pos
|
||||||
|
token_info = ((line - 1, col - 1), len(name))
|
||||||
if token_info:
|
if token_info:
|
||||||
(line, col), length = token_info
|
(line, col), length = token_info
|
||||||
self._append_token((line, col), length, "variable", [TokenModifier.declaration])
|
self._append_token((line, col), length, "variable", [TokenModifier.declaration])
|
||||||
@@ -198,7 +217,9 @@ class _Highlighter(Visitor):
|
|||||||
tokens = []
|
tokens = []
|
||||||
last_line = 0
|
last_line = 0
|
||||||
last_col = 0
|
last_col = 0
|
||||||
for (line, col), length, tok_type, tok_modifier in sorted(self._tokens, key=lambda x: x[0]):
|
raw_tokens = [token for token in self._tokens if token[0] not in self._class_tokens]
|
||||||
|
raw_tokens.extend(self._class_tokens.values())
|
||||||
|
for (line, col), length, tok_type, tok_modifier in sorted(raw_tokens, key=lambda x: x[0]):
|
||||||
line_delta = line - last_line
|
line_delta = line - last_line
|
||||||
col_delta = col
|
col_delta = col
|
||||||
if line == last_line:
|
if line == last_line:
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,79 @@
|
|||||||
|
"""Parameter presentation for statically resolved TclOO calls."""
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
import lsprotocol.types as lsp
|
||||||
|
from tclint.lexer import TclSyntaxError
|
||||||
|
from tclint.syntax_tree import BracedWord, Command
|
||||||
|
|
||||||
|
from tools.parser import CustomParser
|
||||||
|
from tools.signature_help import _active_argument, _contains_cursor
|
||||||
|
from tools.tcloo_completion import resolved_method_calls
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class MethodParameter:
|
||||||
|
name: str
|
||||||
|
label: str
|
||||||
|
variadic: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
def method_parameters(parameters: str) -> list[MethodParameter]:
|
||||||
|
parser = CustomParser()
|
||||||
|
try:
|
||||||
|
words = parser.parse_list(BracedWord(parameters, pos=(1, 1))).children
|
||||||
|
result = []
|
||||||
|
for index, word in enumerate(words):
|
||||||
|
parts = parser.parse_list(word).children
|
||||||
|
if not parts or len(parts) > 2:
|
||||||
|
return []
|
||||||
|
name = parts[0].contents
|
||||||
|
if name is None:
|
||||||
|
return []
|
||||||
|
variadic = name == "args" and len(parts) == 1 and index == len(words) - 1
|
||||||
|
label = "{" + word.contents + "}" if len(parts) == 2 else name
|
||||||
|
result.append(MethodParameter(name, label, variadic))
|
||||||
|
return result
|
||||||
|
except TclSyntaxError:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def method_signature_help(source: str, position: lsp.Position) -> lsp.SignatureHelp | None:
|
||||||
|
lines = source.split("\n")
|
||||||
|
if position.line >= len(lines):
|
||||||
|
return None
|
||||||
|
# AST columns are codepoints; LSP columns are UTF-16 code units.
|
||||||
|
prefix = lines[position.line].encode("utf-16-le")[:position.character * 2].decode("utf-16-le", errors="ignore")
|
||||||
|
cursor = (position.line, len(prefix))
|
||||||
|
candidates = [call for call in resolved_method_calls(source)
|
||||||
|
if _contains_cursor(call.command, lines, cursor)]
|
||||||
|
if not candidates:
|
||||||
|
return None
|
||||||
|
call = max(candidates, key=lambda candidate: candidate.command.pos)
|
||||||
|
|
||||||
|
def nested_active(node):
|
||||||
|
return any(
|
||||||
|
isinstance(child, Command) and _contains_cursor(child, lines, cursor)
|
||||||
|
or nested_active(child)
|
||||||
|
for child in node.children
|
||||||
|
)
|
||||||
|
|
||||||
|
# Let the inner command's own signature provider handle its arguments.
|
||||||
|
if nested_active(call.command):
|
||||||
|
return None
|
||||||
|
argument = _active_argument(call.command, cursor) - call.argument_offset
|
||||||
|
if argument < 0:
|
||||||
|
return None
|
||||||
|
parameters = method_parameters(call.parameters)
|
||||||
|
label = call.label
|
||||||
|
infos = []
|
||||||
|
for parameter in parameters:
|
||||||
|
label += " "
|
||||||
|
start = len(label.encode("utf-16-le")) // 2
|
||||||
|
label += parameter.label
|
||||||
|
infos.append(lsp.ParameterInformation(label=(start, len(label.encode("utf-16-le")) // 2)))
|
||||||
|
active = min(argument, len(parameters) - 1) if parameters else None
|
||||||
|
return lsp.SignatureHelp(
|
||||||
|
signatures=[lsp.SignatureInformation(label=label, parameters=infos, active_parameter=active)],
|
||||||
|
active_signature=0, active_parameter=active,
|
||||||
|
)
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
"""Conservative, document-local TclOO type inference without executing Tcl."""
|
||||||
|
|
||||||
|
from collections.abc import Sequence
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
import re
|
||||||
|
|
||||||
|
import lsprotocol.types as lsp
|
||||||
|
from tclint.lexer import TclSyntaxError
|
||||||
|
from tclint.syntax_tree import BracedWord, Command, CommandSub, Script, VarSub
|
||||||
|
|
||||||
|
from tools.parser import CustomParser
|
||||||
|
from tools.tcl_command_completion import line_prefix_at_position
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ClassInfo:
|
||||||
|
methods: dict[str, tuple[str, Script | None]] = field(default_factory=dict)
|
||||||
|
namespace: str = ""
|
||||||
|
constructor: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MethodCall:
|
||||||
|
command: Command
|
||||||
|
label: str
|
||||||
|
parameters: str
|
||||||
|
argument_offset: int = 1
|
||||||
|
|
||||||
|
|
||||||
|
def parse_completion_source(source, pos=None):
|
||||||
|
# Complete open delimiters while editing; never evaluate the user's code.
|
||||||
|
for _ in range(16):
|
||||||
|
try:
|
||||||
|
return CustomParser().parse(source, pos=pos)
|
||||||
|
except TclSyntaxError as error:
|
||||||
|
message = str(error)
|
||||||
|
closing = next((char for text, char in (
|
||||||
|
("end of command substitution", "]"),
|
||||||
|
("match for brace", "}"), ("match for quote", '"'),
|
||||||
|
) if text in message), None)
|
||||||
|
if closing is None:
|
||||||
|
return None
|
||||||
|
source += closing
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _body(node):
|
||||||
|
if isinstance(node, Script):
|
||||||
|
return node
|
||||||
|
if isinstance(node, BracedWord):
|
||||||
|
return parse_completion_source(node.contents, node.contents_pos)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _qualified(name, namespace):
|
||||||
|
return name if name.startswith("::") else f"{namespace}::{name}"
|
||||||
|
|
||||||
|
|
||||||
|
def tcloo_completions(
|
||||||
|
source_lines: Sequence[str], position: lsp.Position,
|
||||||
|
) -> list[lsp.CompletionItem] | None:
|
||||||
|
"""Return receiver-specific methods, or None outside a known OO context."""
|
||||||
|
prefix = line_prefix_at_position(source_lines, position)
|
||||||
|
if prefix is None:
|
||||||
|
return None
|
||||||
|
match = re.search(r"[\w:]*$", prefix)
|
||||||
|
typed = match.group()
|
||||||
|
# Only a method word, never a variable substitution or method argument.
|
||||||
|
word_start = len(prefix) - len(typed)
|
||||||
|
if word_start == 0 or prefix[word_start - 1] not in " \t":
|
||||||
|
return None
|
||||||
|
marker = "__nx_tcloo_completion_cursor__"
|
||||||
|
lines = list(source_lines)
|
||||||
|
suffix = lines[position.line][len(prefix):]
|
||||||
|
remaining = re.match(r"[\w:]*", suffix).group()
|
||||||
|
lines[position.line] = prefix + marker + suffix[len(remaining):]
|
||||||
|
tree = parse_completion_source("\n".join(lines))
|
||||||
|
if tree is None:
|
||||||
|
return None
|
||||||
|
classes, result, _ = _analyze(tree, typed, marker)
|
||||||
|
if result is None:
|
||||||
|
return None
|
||||||
|
cls, internal = result
|
||||||
|
methods = classes[cls].methods if cls else {"new": ("args", None), "create": ("name args", None)}
|
||||||
|
methods = dict(methods)
|
||||||
|
if cls:
|
||||||
|
methods.setdefault("destroy", ("", None))
|
||||||
|
suffix = source_lines[position.line][len(prefix):]
|
||||||
|
remaining = re.match(r"[\w:]*", suffix).group()
|
||||||
|
start = position.character - len(typed.encode("utf-16-le")) // 2
|
||||||
|
end = position.character + len(remaining.encode("utf-16-le")) // 2
|
||||||
|
return [lsp.CompletionItem(
|
||||||
|
label=name, kind=lsp.CompletionItemKind.Method,
|
||||||
|
detail=f"{cls or 'class'} {name} {signature}".rstrip(),
|
||||||
|
text_edit=lsp.TextEdit(range=lsp.Range(
|
||||||
|
start=lsp.Position(line=position.line, character=start),
|
||||||
|
end=lsp.Position(line=position.line, character=end)), new_text=name),
|
||||||
|
) for name, (signature, _) in sorted(methods.items())
|
||||||
|
if name.startswith(typed) and (internal or not name.startswith("_") and not name[:1].isupper())]
|
||||||
|
|
||||||
|
|
||||||
|
def _analyze(tree, typed="", marker=""):
|
||||||
|
classes = {}
|
||||||
|
contexts = []
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
def collect(script, namespace=""):
|
||||||
|
if script is None:
|
||||||
|
return
|
||||||
|
for cmd in script.children:
|
||||||
|
if not isinstance(cmd, Command):
|
||||||
|
continue
|
||||||
|
args = cmd.args
|
||||||
|
routine = (cmd.routine.contents or "").removeprefix("::")
|
||||||
|
if routine == "namespace" and len(args) == 3 and args[0].contents == "eval" and args[1].contents:
|
||||||
|
collect(_body(args[2]), _qualified(args[1].contents, namespace))
|
||||||
|
elif routine == "oo::class" and len(args) == 3 and args[0].contents == "create" and args[1].contents:
|
||||||
|
name = _qualified(args[1].contents, namespace)
|
||||||
|
info = classes.setdefault(name, ClassInfo(namespace=namespace))
|
||||||
|
body = _body(args[2])
|
||||||
|
if body is None:
|
||||||
|
continue
|
||||||
|
for method in body.children:
|
||||||
|
if not isinstance(method, Command):
|
||||||
|
continue
|
||||||
|
ma = method.args
|
||||||
|
if method.routine.contents == "method" and len(ma) == 3 and ma[0].contents:
|
||||||
|
method_body = _body(ma[2])
|
||||||
|
info.methods[ma[0].contents] = (ma[1].contents or "", method_body)
|
||||||
|
contexts.append((method_body, namespace, name))
|
||||||
|
elif method.routine.contents in {"constructor", "destructor"} and ma:
|
||||||
|
if method.routine.contents == "constructor" and len(ma) == 2:
|
||||||
|
info.constructor = ma[0].contents or ""
|
||||||
|
contexts.append((_body(ma[-1]), namespace, name))
|
||||||
|
|
||||||
|
collect(tree)
|
||||||
|
result = None
|
||||||
|
|
||||||
|
def receiver(node, env, objects, namespace, owner, depth=0):
|
||||||
|
if depth > 12:
|
||||||
|
return None
|
||||||
|
if isinstance(node, VarSub):
|
||||||
|
return env.get(node.value)
|
||||||
|
if isinstance(node, CommandSub) and len(node.children) == 1:
|
||||||
|
return returned(node.children[0], env, objects, namespace, owner, depth + 1)
|
||||||
|
name = node.contents
|
||||||
|
return objects.get(_qualified(name, namespace)) if name else None
|
||||||
|
|
||||||
|
def returned(cmd, env, objects, namespace, owner, depth=0):
|
||||||
|
if not isinstance(cmd, Command) or depth > 12:
|
||||||
|
return None
|
||||||
|
args = cmd.args
|
||||||
|
name = cmd.routine.contents
|
||||||
|
if name == "self" and not args:
|
||||||
|
return owner
|
||||||
|
qualified = _qualified(name, namespace) if name else None
|
||||||
|
if qualified in classes and args and args[0].contents in {"new", "create"}:
|
||||||
|
return qualified
|
||||||
|
cls = owner if name == "my" else receiver(cmd.routine, env, objects, namespace, owner, depth + 1)
|
||||||
|
if cls not in classes or not args:
|
||||||
|
return None
|
||||||
|
method = classes[cls].methods.get(args[0].contents)
|
||||||
|
if method is None or method[1] is None:
|
||||||
|
return None
|
||||||
|
# Only infer unconditional final returns; conditional results stay unknown.
|
||||||
|
commands = [c for c in method[1].children if isinstance(c, Command)]
|
||||||
|
if commands and commands[-1].routine.contents == "return" and len(commands[-1].args) == 1:
|
||||||
|
return receiver(commands[-1].args[0], {}, objects, classes[cls].namespace, cls, depth + 1)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def walk(script, env, objects, namespace="", owner=None):
|
||||||
|
nonlocal result
|
||||||
|
if script is None:
|
||||||
|
return
|
||||||
|
for cmd in script.children:
|
||||||
|
if not isinstance(cmd, Command):
|
||||||
|
continue
|
||||||
|
args = cmd.args
|
||||||
|
name = cmd.routine.contents
|
||||||
|
cls = owner if name == "my" else receiver(cmd.routine, env, objects, namespace, owner)
|
||||||
|
method_name = args[0].contents if args else None
|
||||||
|
if cls in classes and method_name in classes[cls].methods:
|
||||||
|
calls.append(MethodCall(cmd, f"{cls} {method_name}", classes[cls].methods[method_name][0]))
|
||||||
|
elif name and _qualified(name, namespace) in classes and method_name in {"new", "create"}:
|
||||||
|
cls = _qualified(name, namespace)
|
||||||
|
parameters = classes[cls].constructor
|
||||||
|
if method_name == "create":
|
||||||
|
parameters = "objectName " + parameters
|
||||||
|
calls.append(MethodCall(cmd, f"{cls} {method_name}", parameters))
|
||||||
|
if marker and args and args[0].contents == typed + marker:
|
||||||
|
cls = owner if name == "my" else receiver(cmd.routine, env, objects, namespace, owner)
|
||||||
|
if cls in classes:
|
||||||
|
result = (cls, name == "my")
|
||||||
|
elif name and _qualified(name, namespace) in classes:
|
||||||
|
result = (None, False)
|
||||||
|
return
|
||||||
|
# Command substitutions can contain the completion receiver.
|
||||||
|
for node in cmd.children:
|
||||||
|
if isinstance(node, CommandSub):
|
||||||
|
walk(node, env, objects, namespace, owner)
|
||||||
|
if name == "set" and len(args) == 2 and args[0].contents:
|
||||||
|
env[args[0].contents] = receiver(args[1], env, objects, namespace, owner)
|
||||||
|
elif name == "unset":
|
||||||
|
for arg in args:
|
||||||
|
env.pop(arg.contents, None)
|
||||||
|
elif name == "proc" and len(args) == 3:
|
||||||
|
walk(_body(args[2]), {}, objects.copy(), namespace)
|
||||||
|
elif name == "namespace" and len(args) == 3 and args[0].contents == "eval" and args[1].contents:
|
||||||
|
walk(_body(args[2]), {}, objects, _qualified(args[1].contents, namespace))
|
||||||
|
elif name and _qualified(name, namespace) in classes and len(args) >= 2 and args[0].contents == "create" and args[1].contents:
|
||||||
|
objects[_qualified(args[1].contents, namespace)] = _qualified(name, namespace)
|
||||||
|
else:
|
||||||
|
for arg in args:
|
||||||
|
if isinstance(arg, Script):
|
||||||
|
# Branch-local facts are not propagated beyond the branch.
|
||||||
|
walk(arg, env.copy(), objects.copy(), namespace, owner)
|
||||||
|
|
||||||
|
walk(tree, {}, {})
|
||||||
|
for body, namespace, owner in contexts:
|
||||||
|
walk(body, {}, {}, namespace, owner)
|
||||||
|
return classes, result, calls
|
||||||
|
|
||||||
|
|
||||||
|
def resolved_method_calls(source):
|
||||||
|
tree = parse_completion_source(source)
|
||||||
|
return _analyze(tree)[2] if tree is not None else []
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
"""Class declarations and references shared by completion and highlighting."""
|
||||||
|
|
||||||
|
import lsprotocol.types as lsp
|
||||||
|
from tclint.syntax_tree import Command
|
||||||
|
|
||||||
|
from tools.tcloo_completion import _body, _qualified
|
||||||
|
|
||||||
|
|
||||||
|
def class_symbols(tree):
|
||||||
|
"""Return qualified class declarations and statically resolved name nodes."""
|
||||||
|
declarations = {}
|
||||||
|
commands = []
|
||||||
|
|
||||||
|
def walk(node, namespace="", in_class=False):
|
||||||
|
if node is None:
|
||||||
|
return
|
||||||
|
if isinstance(node, Command):
|
||||||
|
args = node.args
|
||||||
|
name = (node.routine.contents or "").removeprefix("::")
|
||||||
|
commands.append((node, namespace))
|
||||||
|
if (name == "namespace" and len(args) == 3
|
||||||
|
and args[0].contents == "eval" and args[1].contents):
|
||||||
|
walk(_body(args[2]), _qualified(args[1].contents, namespace), in_class)
|
||||||
|
return
|
||||||
|
if (name == "oo::class" and len(args) == 3
|
||||||
|
and args[0].contents == "create" and args[1].contents):
|
||||||
|
declarations[_qualified(args[1].contents, namespace)] = args[1]
|
||||||
|
walk(_body(args[2]), namespace, True)
|
||||||
|
return
|
||||||
|
if in_class and name in {"method", "constructor", "destructor"} and args:
|
||||||
|
walk(_body(args[-1]), namespace, True)
|
||||||
|
return
|
||||||
|
for child in node.children:
|
||||||
|
walk(child, namespace, in_class)
|
||||||
|
|
||||||
|
walk(tree)
|
||||||
|
references = []
|
||||||
|
for command, namespace in commands:
|
||||||
|
name = command.routine.contents
|
||||||
|
if name and any(candidate in declarations for candidate in (
|
||||||
|
_qualified(name, namespace), _qualified(name, ""),
|
||||||
|
)):
|
||||||
|
references.append(command.routine)
|
||||||
|
return declarations, references
|
||||||
|
|
||||||
|
|
||||||
|
def class_completion_items(tree):
|
||||||
|
declarations, _ = class_symbols(tree)
|
||||||
|
return [lsp.CompletionItem(
|
||||||
|
label=name.removeprefix("::"),
|
||||||
|
kind=lsp.CompletionItemKind.Class,
|
||||||
|
detail=f"TclOO class {name}",
|
||||||
|
) for name in sorted(declarations)]
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
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
|
||||||
|
from tools.variable_names import variable_name
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -18,13 +19,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 +42,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 +56,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,
|
||||||
@@ -71,7 +82,7 @@ def build_variable_index(source: str, tree: Node | None = None) -> tuple[Set[str
|
|||||||
return
|
return
|
||||||
|
|
||||||
if routine == "set" and node.args:
|
if routine == "set" and node.args:
|
||||||
raw_name = getattr(node.args[0], "contents", None)
|
raw_name = variable_name(node.args[0])
|
||||||
base = _normalize_var_name(raw_name)
|
base = _normalize_var_name(raw_name)
|
||||||
if base is not None:
|
if base is not None:
|
||||||
if raw_name and raw_name.startswith("::"):
|
if raw_name and raw_name.startswith("::"):
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
"""Extract statically known variable names without evaluating Tcl substitutions."""
|
||||||
|
|
||||||
|
from tclint.syntax_tree import BareWord, CompoundBareWord, Node, QuotedWord, VarSub
|
||||||
|
|
||||||
|
|
||||||
|
def variable_name(node: Node) -> str | None:
|
||||||
|
contents = node.contents
|
||||||
|
if isinstance(contents, str):
|
||||||
|
return contents
|
||||||
|
if isinstance(node, (CompoundBareWord, QuotedWord)) and node.children:
|
||||||
|
first = node.children[0]
|
||||||
|
last = node.children[-1]
|
||||||
|
if (
|
||||||
|
isinstance(first, BareWord)
|
||||||
|
and isinstance(last, BareWord)
|
||||||
|
and "(" in first.value
|
||||||
|
and last.value.endswith(")")
|
||||||
|
):
|
||||||
|
# Substitutions in an array index do not change the array's name.
|
||||||
|
return first.value.split("(", 1)[0] or None
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def array_key_parts(node: Node, *, preserve_variables: bool = False) -> tuple[str | None, ...]:
|
||||||
|
"""Keep comma-separated literal index components; substitutions are unknown."""
|
||||||
|
contents = node.contents
|
||||||
|
if not isinstance(contents, str):
|
||||||
|
if not isinstance(node, (CompoundBareWord, QuotedWord)) or not variable_name(node):
|
||||||
|
return ()
|
||||||
|
chunks = []
|
||||||
|
for child in node.children:
|
||||||
|
if isinstance(child, BareWord):
|
||||||
|
chunks.append(child.value)
|
||||||
|
elif preserve_variables and isinstance(child, VarSub) and not child.children:
|
||||||
|
chunks.append("${" + child.value + "}" if child.braced else "$" + child.value)
|
||||||
|
else:
|
||||||
|
chunks.append("\0")
|
||||||
|
contents = "".join(chunks)
|
||||||
|
if "(" not in contents or not contents.endswith(")"):
|
||||||
|
return ()
|
||||||
|
return tuple(
|
||||||
|
part if part and not any(char in part for char in ("\0[]\\" if preserve_variables else "\0$[]\\")) else None
|
||||||
|
for part in contents.split("(", 1)[1][:-1].split(",")
|
||||||
|
)
|
||||||
@@ -6,18 +6,24 @@ 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 (
|
||||||
|
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:
|
||||||
@@ -90,6 +96,149 @@ def _complete(document: TextDocument, position: lsp.Position):
|
|||||||
).items
|
).items
|
||||||
|
|
||||||
|
|
||||||
|
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 test_array_keys_complete_in_set_and_substitution(tmp_path: Path, monkeypatch):
|
||||||
|
server, _, _ = _completion_server(tmp_path, monkeypatch)
|
||||||
|
workspace = _document(
|
||||||
|
tmp_path / "arrays.tcl",
|
||||||
|
"set ::lib_flag(enabled) 1\nset ::lib_flag(external) 1\n",
|
||||||
|
)
|
||||||
|
assert server.update_poco_completion_for_file(workspace)
|
||||||
|
source = (
|
||||||
|
"set lib_flag(enabled) 0\n"
|
||||||
|
"set lib_flag(empty) 1\n"
|
||||||
|
"set other(wrong) 1\n"
|
||||||
|
"proc hidden {} { set lib_flag(private) 1 }\n"
|
||||||
|
"set lib_flag()\n"
|
||||||
|
"puts $lib_flag(en)\n"
|
||||||
|
"puts 😀; set lib_flag(em\n"
|
||||||
|
)
|
||||||
|
current = _document(tmp_path / "arrays-current.tcl", source)
|
||||||
|
server.workspace.put_text_document(lsp.TextDocumentItem(
|
||||||
|
uri=current.uri, language_id="tcl", version=1, text=source,
|
||||||
|
))
|
||||||
|
assert server.update_poco_completion_for_file(current)
|
||||||
|
items = _complete(current, _position_after(source, "set lib_flag(", 3))
|
||||||
|
assert [item.label for item in items] == ["empty", "enabled", "external"]
|
||||||
|
assert all(item.text_edit.new_text == item.label for item in items)
|
||||||
|
items = _complete(current, _position_after(source, "puts $lib_flag(en"))
|
||||||
|
assert [item.label for item in items] == ["enabled"]
|
||||||
|
assert items[0].text_edit.new_text == "enabled"
|
||||||
|
position = lsp.Position(line=6, character=len(source.splitlines()[6].encode("utf-16-le")) // 2)
|
||||||
|
items = _complete(current, position)
|
||||||
|
assert [item.label for item in items] == ["empty"]
|
||||||
|
assert items[0].text_edit.new_text == "empty)"
|
||||||
|
assert items[0].text_edit.range.start.character == position.character - 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_dynamic_array_index_keeps_variable_identity(tmp_path: Path, monkeypatch):
|
||||||
|
from tools.navigation import build_file_symbol_index
|
||||||
|
from tools.parser import CustomParser
|
||||||
|
from tools.semantic_tokens import _Highlighter
|
||||||
|
from tools.variable_index import build_variable_index
|
||||||
|
from tools.variable_names import variable_name
|
||||||
|
|
||||||
|
source = (
|
||||||
|
"set custom_flag(from_move,$::mom_path_name) 1\n"
|
||||||
|
'set "::quoted_flag(from_move,$::mom_path_name)" 1\n'
|
||||||
|
"set ::command_flag([info hostname]) 1\n"
|
||||||
|
"set ${dynamic_name}(entry) 1\n"
|
||||||
|
"proc example {} { set local_flag($::mom_path_name) 1 }\n"
|
||||||
|
"puts $custom_flag\n"
|
||||||
|
)
|
||||||
|
tree = CustomParser().parse(source)
|
||||||
|
globals_, locals_, _ = build_variable_index(source, tree)
|
||||||
|
assert {"custom_flag", "quoted_flag", "command_flag"} <= globals_
|
||||||
|
assert locals_["example"] == {"local_flag"}
|
||||||
|
assert variable_name(tree.children[3].args[0]) is None
|
||||||
|
path = tmp_path / "dynamic.tcl"
|
||||||
|
index = build_file_symbol_index(str(path), path.as_uri(), tree)
|
||||||
|
definition = next(
|
||||||
|
item for item in index.occurrences
|
||||||
|
if item.identity.name == "::custom_flag" and item.is_definition
|
||||||
|
)
|
||||||
|
assert definition.range.start.character == 4
|
||||||
|
assert definition.range.end.character == 15
|
||||||
|
assert definition.array_element is None
|
||||||
|
assert any(item.identity.name == "::mom_path_name" for item in index.occurrences)
|
||||||
|
highlighter = _Highlighter([], {})
|
||||||
|
tree.accept(highlighter, recurse=True)
|
||||||
|
assert any(
|
||||||
|
position == (0, 4) and length == 11 and kind == "variable"
|
||||||
|
for position, length, kind, _ in highlighter._tokens
|
||||||
|
)
|
||||||
|
server, _, _ = _completion_server(tmp_path, monkeypatch)
|
||||||
|
current = _document(path, source)
|
||||||
|
server.workspace.put_text_document(lsp.TextDocumentItem(
|
||||||
|
uri=current.uri, language_id="tcl", version=1, text=source,
|
||||||
|
))
|
||||||
|
assert server.update_poco_completion_for_file(current)
|
||||||
|
items = _complete(current, _position_after(source, "puts $custom"))
|
||||||
|
assert "custom_flag" in {item.label for item in items}
|
||||||
|
workspace_items = next(
|
||||||
|
items for item_path, items in server.completion_items_by_file_snapshot().items()
|
||||||
|
if server.paths_equal(item_path, str(path))
|
||||||
|
)
|
||||||
|
assert {"quoted_flag", "command_flag"} <= {item.label for item in workspace_items}
|
||||||
|
|
||||||
|
|
||||||
|
def test_literal_array_components_complete_around_substitutions(tmp_path: Path, monkeypatch):
|
||||||
|
from tools.completion_items import array_element_completions
|
||||||
|
|
||||||
|
server, _, _ = _completion_server(tmp_path, monkeypatch)
|
||||||
|
source = (
|
||||||
|
"set custom_flag(from_move,$::mom_path_name) 1\n"
|
||||||
|
"set custom_flag(to_move,$::mom_path_name) 1\n"
|
||||||
|
"set custom_flag($::mom_path_name,finished) 1\n"
|
||||||
|
"set custom_flag(prefix_$::mom_path_name,other) 1\n"
|
||||||
|
"set custom_flag([info hostname],command_tail) 1\n"
|
||||||
|
"set other_flag(wrong,$::mom_path_name) 1\n"
|
||||||
|
"set multi_flag(move,$first,axis,$second) 1\n"
|
||||||
|
)
|
||||||
|
document = _document(tmp_path / "components.tcl", source)
|
||||||
|
assert server.update_poco_completion_for_file(document)
|
||||||
|
cases = [
|
||||||
|
("set custom_flag(|,$::mom_path_name)", {"from_move", "to_move"}, "from_move", "set custom_flag(from_move,$::mom_path_name)"),
|
||||||
|
("set custom_flag(fr|om_old,$::mom_path_name)", {"from_move"}, "from_move", "set custom_flag(from_move,$::mom_path_name)"),
|
||||||
|
("puts $custom_flag($::mom_path_name,fi|)", {"finished"}, "finished", "puts $custom_flag($::mom_path_name,finished)"),
|
||||||
|
("set custom_flag($::mom_path_name,|)", {"finished", "other", "command_tail"}, "other", "set custom_flag($::mom_path_name,other)"),
|
||||||
|
("set custom_flag(fr|", {"from_move"}, "from_move", "set custom_flag(from_move,$::mom_path_name)"),
|
||||||
|
("set custom_flag(fr|)", {"from_move"}, "from_move", "set custom_flag(from_move,$::mom_path_name)"),
|
||||||
|
("set multi_flag(m|)", {"move"}, "move", "set multi_flag(move,$first,axis,$second)"),
|
||||||
|
]
|
||||||
|
for marked, labels, selected, expected in cases:
|
||||||
|
offset = marked.index("|")
|
||||||
|
line = marked.replace("|", "")
|
||||||
|
items = array_element_completions(
|
||||||
|
[line], lsp.Position(line=0, character=offset),
|
||||||
|
server.navigation_snapshot().values(), str(tmp_path / "caller.tcl"),
|
||||||
|
)
|
||||||
|
assert {item.label for item in items} == labels
|
||||||
|
edit = next(item.text_edit for item in items if item.label == selected)
|
||||||
|
item = next(item for item in items if item.label == selected)
|
||||||
|
assert item.insert_text_format == lsp.InsertTextFormat.PlainText
|
||||||
|
assert line[:edit.range.start.character] + edit.new_text + line[edit.range.end.character:] == expected
|
||||||
|
assert array_element_completions(
|
||||||
|
["set custom_flag(from_move,$::mom"], lsp.Position(line=0, character=31),
|
||||||
|
server.navigation_snapshot().values(), str(tmp_path / "caller.tcl"),
|
||||||
|
) is None
|
||||||
|
|
||||||
|
|
||||||
|
def _argument_completion_request(source: str):
|
||||||
|
lines = source.split("\n")
|
||||||
|
character = len(lines[-1].encode("utf-16-le")) // 2
|
||||||
|
return tcl_argument_completion(
|
||||||
|
lines,
|
||||||
|
lsp.Position(line=len(lines) - 1, character=character),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
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 +299,339 @@ 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_string_completion_inside_braced_conditions_and_bodies():
|
||||||
|
for prefix in ("if {", "while {", "proc example {} { if {"):
|
||||||
|
subcommands = _argument_completion_labels(prefix + "[string ")
|
||||||
|
assert subcommands is not None
|
||||||
|
assert {"compare", "equal", "is"} <= subcommands
|
||||||
|
assert _argument_completion_labels(prefix + "[string compare -") == {
|
||||||
|
"-length", "-nocase"
|
||||||
|
}
|
||||||
|
assert _argument_completion_labels(
|
||||||
|
prefix + "[string compare -nocase "
|
||||||
|
) == {"-length"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_closed_braced_arguments_do_not_change_completion_context():
|
||||||
|
assert _argument_completion_labels("puts {[string compare }") is None
|
||||||
|
assert _argument_completion_labels(
|
||||||
|
"if {[string equal a b]} {string compare "
|
||||||
|
) == {"-length", "-nocase"}
|
||||||
|
assert _argument_completion_labels(
|
||||||
|
"if {[string equal a b] && [string is integer "
|
||||||
|
) == {"-failindex", "-strict"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_dict_array_namespace_file_and_info_subcommands():
|
||||||
|
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 == []
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import lsprotocol.types as lsp
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from tools.inlay_hint import InlayHintGenerator
|
||||||
|
from tools.parser import CustomParser
|
||||||
|
from tools.tcloo_arguments import method_parameters, method_signature_help
|
||||||
|
|
||||||
|
|
||||||
|
CLASS = """oo::class create MCS {
|
||||||
|
constructor {name {size 3}} {}
|
||||||
|
method initValue {i value} {return [self]}
|
||||||
|
method format {value {precision 7}} {}
|
||||||
|
method many {first args} {}
|
||||||
|
method empty {} {}
|
||||||
|
method internal {} {my initValue 0 10}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def signature(source):
|
||||||
|
prefix, suffix = source.split("|")
|
||||||
|
position = lsp.Position(line=prefix.count("\n"), character=len(prefix.rsplit("\n", 1)[-1].encode("utf-16-le")) // 2)
|
||||||
|
return method_signature_help(prefix + suffix, position)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("tail, label, active", [
|
||||||
|
("set mcs [MCS new test]\n$mcs initValue |", "::MCS initValue i value", 0),
|
||||||
|
("set mcs [MCS new test]\n$mcs initValue 0 |", "::MCS initValue i value", 1),
|
||||||
|
("set mcs [MCS new test]\n$mcs format 12 |", "::MCS format value {precision 7}", 1),
|
||||||
|
("set mcs [MCS new test]\n$mcs many 1 2 3 |", "::MCS many first args", 1),
|
||||||
|
("set mcs [MCS new test]\n$mcs empty |", "::MCS empty", None),
|
||||||
|
("MCS create instance test\ninstance initValue 0 |", "::MCS initValue i value", 1),
|
||||||
|
("set mcs [[MCS new test] initValue 0 0]\n$mcs initValue |", "::MCS initValue i value", 0),
|
||||||
|
("set mcs [MCS new test]\nputs [$mcs initValue 0 |", "::MCS initValue i value", 1),
|
||||||
|
("set mcs [MCS new |", "::MCS new name {size 3}", 0),
|
||||||
|
("MCS create instance |", "::MCS create objectName name {size 3}", 1),
|
||||||
|
("set mcs [MCS new test]\nputs 😀; $mcs initValue 0 |", "::MCS initValue i value", 1),
|
||||||
|
])
|
||||||
|
def test_method_signatures(tail, label, active):
|
||||||
|
result = signature(CLASS + tail)
|
||||||
|
assert result is not None
|
||||||
|
assert result.signatures[0].label == label
|
||||||
|
assert result.active_parameter == active
|
||||||
|
|
||||||
|
|
||||||
|
def test_my_signature_and_parameter_spans():
|
||||||
|
result = signature(CLASS.replace("my initValue 0 10", "my initValue 0 |"))
|
||||||
|
assert result.active_parameter == 1
|
||||||
|
info = result.signatures[0]
|
||||||
|
assert [info.label[start:end] for start, end in (p.label for p in info.parameters)] == ["i", "value"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("tail", [
|
||||||
|
"$unknown initValue |",
|
||||||
|
"set mcs [MCS new test]\nset mcs text\n$mcs initValue |",
|
||||||
|
"set mcs [MCS new test]\n$mcs initValue [unknown |] 3",
|
||||||
|
"set mcs [MCS new test]\n$mcs initValue|",
|
||||||
|
])
|
||||||
|
def test_unknown_receivers_and_nested_commands(tail):
|
||||||
|
assert signature(CLASS + tail) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_optional_parameter_with_spaced_default():
|
||||||
|
parameters = method_parameters('value {description {hello world}} args')
|
||||||
|
assert [p.name for p in parameters] == ["value", "description", "args"]
|
||||||
|
assert parameters[1].label == "{description {hello world}}"
|
||||||
|
assert parameters[-1].variadic
|
||||||
|
|
||||||
|
|
||||||
|
def test_method_inlay_hints_and_existing_preferences():
|
||||||
|
source = CLASS + "set mcs [MCS new test]\n$mcs initValue 0 10\n$mcs many 1 2 3\n$mcs initValue $i $other"
|
||||||
|
tree = CustomParser().parse(source)
|
||||||
|
start = len(CLASS.splitlines()) + 1
|
||||||
|
requested = lsp.Range(start=lsp.Position(line=start, character=0), end=lsp.Position(line=start + 2, character=100))
|
||||||
|
generator = InlayHintGenerator(source, {}, requested_range=requested)
|
||||||
|
hints = generator.generate(tree)
|
||||||
|
assert [hint.label[0].value for hint in hints] == ["i:", "value:", "first:", "args:", "args:", "value:"]
|
||||||
|
assert [source.splitlines()[hint.position.line][hint.position.character:] for hint in hints[:2]] == ["0 10", "10"]
|
||||||
|
assert "::MCS initValue i value" in hints[0].tooltip.value
|
||||||
|
generator = InlayHintGenerator(source, {}, requested_range=requested, parameter_names="literals")
|
||||||
|
assert len(generator.generate(tree)) == 5
|
||||||
|
generator = InlayHintGenerator(source, {}, parameter_names="none")
|
||||||
|
assert generator.generate(tree) == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_lsp_signature_help_with_unfinished_bracket(tmp_path, monkeypatch):
|
||||||
|
import lsp_server
|
||||||
|
from test_completion_context import _completion_server
|
||||||
|
|
||||||
|
server, document, _ = _completion_server(tmp_path, monkeypatch)
|
||||||
|
source = CLASS + "set mcs [MCS new test]\nputs [$mcs initValue 0 "
|
||||||
|
document = server.workspace.get_text_document(document.uri)
|
||||||
|
document._source = source
|
||||||
|
document.version = 2
|
||||||
|
result = lsp_server.signature_help(lsp.SignatureHelpParams(
|
||||||
|
text_document=lsp.TextDocumentIdentifier(uri=document.uri),
|
||||||
|
position=lsp.Position(line=len(source.splitlines()) - 1, character=len(source.splitlines()[-1])),
|
||||||
|
))
|
||||||
|
assert result.active_parameter == 1
|
||||||
|
assert result.signatures[0].label == "::MCS initValue i value"
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
import lsprotocol.types as lsp
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from tools.tcloo_completion import tcloo_completions
|
||||||
|
|
||||||
|
|
||||||
|
CLASS = """oo::class create MCS {
|
||||||
|
constructor {} {my initValue 0 0}
|
||||||
|
method initValue {i value} {return [self]}
|
||||||
|
method initOrg {x y z} {return [my initValue 0 $x]}
|
||||||
|
method toLst {} {return {1 2 3}}
|
||||||
|
method _private {} {return [self]}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def complete(source):
|
||||||
|
offset = source.index("|")
|
||||||
|
before = source[:offset]
|
||||||
|
position = lsp.Position(line=before.count("\n"), character=len(before.rsplit("\n", 1)[-1].encode("utf-16-le")) // 2)
|
||||||
|
return tcloo_completions(source.replace("|", "").splitlines(), position)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("code", [
|
||||||
|
"set mcs [MCS new]\n$mcs |",
|
||||||
|
"set mcs [::MCS new]\n$mcs |",
|
||||||
|
"MCS create instance\ninstance |",
|
||||||
|
"set mcs [MCS create instance]\n$mcs |",
|
||||||
|
"set mcs [[MCS new] initValue 0 0]\n$mcs |",
|
||||||
|
"set mcs [[MCS new] initOrg 1 2 3]\n$mcs |",
|
||||||
|
"set a [MCS new]\nset mcs $a\n$mcs |",
|
||||||
|
"proc run {} {set mcs [MCS new]; $mcs |}",
|
||||||
|
"set mcs [MCS new]\nputs [$mcs |]",
|
||||||
|
"set mcs [MCS new]\nputs [$mcs |",
|
||||||
|
"[MCS new] |",
|
||||||
|
])
|
||||||
|
def test_instances_and_chains(code):
|
||||||
|
items = complete(CLASS + code)
|
||||||
|
assert {item.label for item in items} == {"initValue", "initOrg", "toLst", "destroy"}
|
||||||
|
assert next(item for item in items if item.label == "initValue").detail == "::MCS initValue i value"
|
||||||
|
|
||||||
|
|
||||||
|
def test_my_and_constructor():
|
||||||
|
for source in [CLASS.replace("my initValue 0 0", "my |"), CLASS.replace("return {1 2 3}", "my |")]:
|
||||||
|
assert "_private" in {item.label for item in complete(source)}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("code", [
|
||||||
|
"set mcs [MCS new]\nset mcs text\n$mcs |",
|
||||||
|
"set mcs [MCS new]\nunset mcs\n$mcs |",
|
||||||
|
"proc a {} {set mcs [MCS new]}\nproc b {} {$mcs |}",
|
||||||
|
"set mcs [MCS new]\nproc b {mcs} {$mcs |}",
|
||||||
|
"set mcs [[MCS new] toLst]\n$mcs |",
|
||||||
|
"$mcs |\nset mcs [MCS new]",
|
||||||
|
"set mcs [MCS new]\n$mcs initValue |",
|
||||||
|
"# my |",
|
||||||
|
"puts {my |}",
|
||||||
|
])
|
||||||
|
def test_unknown_and_non_command_contexts(code):
|
||||||
|
assert complete(CLASS + code) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_prefix_and_replacement():
|
||||||
|
items = complete(CLASS + "set mcs [MCS new]\n$mcs initV|alue")
|
||||||
|
assert [item.label for item in items] == ["initValue"]
|
||||||
|
assert items[0].text_edit.new_text == "initValue"
|
||||||
|
assert items[0].text_edit.range.start.character == 5
|
||||||
|
assert items[0].text_edit.range.end.character == 14
|
||||||
|
|
||||||
|
|
||||||
|
def test_namespace():
|
||||||
|
source = "namespace eval geometry {\n" + CLASS + "set mcs [MCS new]\n$mcs |\n}"
|
||||||
|
assert "initOrg" in {item.label for item in complete(source)}
|
||||||
|
|
||||||
|
|
||||||
|
def test_lsp_space_trigger(tmp_path, monkeypatch):
|
||||||
|
import lsp_server
|
||||||
|
from test_completion_context import _completion_server
|
||||||
|
|
||||||
|
server, document, _ = _completion_server(tmp_path, monkeypatch)
|
||||||
|
source = CLASS + "set mcs [MCS new]\n$mcs "
|
||||||
|
document = server.workspace.get_text_document(document.uri)
|
||||||
|
document._source = source
|
||||||
|
document.version = 2
|
||||||
|
items = lsp_server.on_completion(lsp.CompletionParams(
|
||||||
|
text_document=lsp.TextDocumentIdentifier(uri=document.uri),
|
||||||
|
position=lsp.Position(line=len(source.splitlines()) - 1, character=5),
|
||||||
|
context=lsp.CompletionContext(trigger_kind=lsp.CompletionTriggerKind.TriggerCharacter, trigger_character=" "),
|
||||||
|
)).items
|
||||||
|
assert "initOrg" in {item.label for item in items}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("tail", ["set mcs [MC", "set mcs [MC]", "MC"])
|
||||||
|
def test_class_name_completion_while_editing(tmp_path, monkeypatch, tail):
|
||||||
|
import lsp_server
|
||||||
|
from test_completion_context import _completion_server
|
||||||
|
|
||||||
|
server, document, _ = _completion_server(tmp_path, monkeypatch)
|
||||||
|
source = CLASS + tail
|
||||||
|
document = server.workspace.get_text_document(document.uri)
|
||||||
|
document._source = source
|
||||||
|
document.version = 2
|
||||||
|
items = lsp_server.on_completion(lsp.CompletionParams(
|
||||||
|
text_document=lsp.TextDocumentIdentifier(uri=document.uri),
|
||||||
|
position=lsp.Position(line=len(source.splitlines()) - 1,
|
||||||
|
character=len(tail.rstrip("]"))),
|
||||||
|
)).items
|
||||||
|
classes = [item for item in items if item.label == "MCS"]
|
||||||
|
assert len(classes) == 1
|
||||||
|
assert classes[0].kind == lsp.CompletionItemKind.Class
|
||||||
|
|
||||||
|
|
||||||
|
def test_classes_are_indexed_with_their_own_kind(tmp_path, monkeypatch):
|
||||||
|
from test_completion_context import _completion_server, _document
|
||||||
|
|
||||||
|
server, _, _ = _completion_server(tmp_path, monkeypatch)
|
||||||
|
assert server.update_poco_completion_for_file(_document(tmp_path / "class.tcl", CLASS))
|
||||||
|
items = server.completion_items_snapshot()
|
||||||
|
assert any(item.label == "MCS" and item.kind == lsp.CompletionItemKind.Class for item in items)
|
||||||
|
assert "MCS" not in server.custom_function_names_snapshot()
|
||||||
|
|
||||||
|
|
||||||
|
def test_class_command_completions():
|
||||||
|
assert {item.label for item in complete(CLASS + "set mcs [MCS |]")} == {"new", "create"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_semantic_class_declarations_and_uses():
|
||||||
|
from tools.parser import CustomParser
|
||||||
|
from tools.semantic_tokens import _Highlighter, TokenModifier
|
||||||
|
|
||||||
|
source = CLASS + 'set mcs [MCS new]\nset second [::MCS new]\nputs "MCS"\n'
|
||||||
|
tree = CustomParser().parse(source)
|
||||||
|
highlighter = _Highlighter([], {"MCS"})
|
||||||
|
highlighter.highlight_classes(tree)
|
||||||
|
tree.accept(highlighter, recurse=True)
|
||||||
|
line = col = 0
|
||||||
|
classified = []
|
||||||
|
for token in highlighter.tokens():
|
||||||
|
col = col + token.offset if token.line == 0 else token.offset
|
||||||
|
line += token.line
|
||||||
|
text = source.splitlines()[line][col:col + token.length]
|
||||||
|
classified.append((line, col, text, token.tok_type, token.tok_modifiers))
|
||||||
|
classes = [entry for entry in classified if entry[3] == "class"]
|
||||||
|
assert [entry[2] for entry in classes] == ["MCS", "MCS", "::MCS"]
|
||||||
|
assert TokenModifier.declaration in classes[0][4]
|
||||||
|
assert not any(entry[2] in {"MCS", "::MCS"} and entry[3] == "function" for entry in classified)
|
||||||
|
assert len({entry[:2] for entry in classified}) == len(classified)
|
||||||
|
|
||||||
|
|
||||||
|
def test_namespaced_class_symbols_and_method_body_references():
|
||||||
|
from tools.parser import CustomParser
|
||||||
|
from tools.tcloo_symbols import class_completion_items, class_symbols
|
||||||
|
|
||||||
|
tree = CustomParser().parse('''namespace eval geometry {
|
||||||
|
oo::class create MCS {
|
||||||
|
method duplicate {} {return [MCS new]}
|
||||||
|
}
|
||||||
|
set mcs [MCS new]
|
||||||
|
}
|
||||||
|
set mcs [geometry::MCS new]
|
||||||
|
set mcs [::geometry::MCS new]
|
||||||
|
''')
|
||||||
|
declarations, references = class_symbols(tree)
|
||||||
|
assert set(declarations) == {"::geometry::MCS"}
|
||||||
|
assert [node.contents for node in references] == ["MCS", "MCS", "geometry::MCS", "::geometry::MCS"]
|
||||||
|
assert class_completion_items(tree)[0].label == "geometry::MCS"
|
||||||
@@ -5,6 +5,9 @@
|
|||||||
{
|
{
|
||||||
"include": "#comment"
|
"include": "#comment"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"include": "#escaped-quoted-strings"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"include": "#strings"
|
"include": "#strings"
|
||||||
},
|
},
|
||||||
@@ -28,6 +31,31 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"repository": {
|
"repository": {
|
||||||
|
"escaped-quoted-strings": {
|
||||||
|
"name": "string.quoted.double.def",
|
||||||
|
"begin": "\\\\\"",
|
||||||
|
"end": "\\\\\"",
|
||||||
|
"beginCaptures": {
|
||||||
|
"0": {
|
||||||
|
"name": "punctuation.definition.string.begin.def"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"endCaptures": {
|
||||||
|
"0": {
|
||||||
|
"name": "punctuation.definition.string.end.def"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"name": "constant.character.escape.def",
|
||||||
|
"match": "\\\\."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "variable.other.property",
|
||||||
|
"match": "\\$[_a-zA-Z][_a-zA-Z0-9]*(\\(\\$?[_a-zA-Z][_a-zA-Z0-9]*(,\\$?[_a-zA-Z][_a-zA-Z0-9]*)*\\))?"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"keywords": {
|
"keywords": {
|
||||||
"patterns": [
|
"patterns": [
|
||||||
{
|
{
|
||||||
|
|||||||
+280
-1
@@ -117,10 +117,289 @@ 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
|
||||||
|
|
||||||
|
|
||||||
SERVICE_remove_file "test"
|
SERVICE_remove_file "test"
|
||||||
|
|
||||||
|
|
||||||
|
oo::class create MCS {
|
||||||
|
variable values
|
||||||
|
|
||||||
|
constructor {args} {
|
||||||
|
for {set i 0} {$i <= 11} {incr i} {
|
||||||
|
my initValue $i 0.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
destructor {
|
||||||
|
#puts "[self] is now deleted"
|
||||||
|
}
|
||||||
|
|
||||||
|
method fformat {value {precision 7}} {return [expr {round( 10.0 ** $precision * $value) / (10.0 ** $precision)}]}
|
||||||
|
method radtodeg {rad} {return [expr {$rad*180.0/(4.0*atan(1.0))}]}
|
||||||
|
method degtorad {deg} {return [expr {$deg*(4.0*atan(1.0))/180.0}]}
|
||||||
|
|
||||||
|
method duplicate {args} {return [[MCS new] initMCS [self]]}
|
||||||
|
method type {args} {return "::MCS"}
|
||||||
|
|
||||||
|
method toArray {args} {
|
||||||
|
for {set i 0} {$i < [array size values]} {incr i} {
|
||||||
|
append arr [string cat $i " "] ; append arr [string cat $values($i) " "]
|
||||||
|
}
|
||||||
|
return [string trimright $arr]
|
||||||
|
}
|
||||||
|
method toLst {args} {
|
||||||
|
for {set i 0} {$i < [array size values]} {incr i} {
|
||||||
|
lappend lst $values($i)
|
||||||
|
}
|
||||||
|
return $lst
|
||||||
|
}
|
||||||
|
method toStr {args} {
|
||||||
|
for {set i 0} {$i < [array size values]} {incr i} {
|
||||||
|
if {$i>0} {set str [string cat $str ", "]}
|
||||||
|
append str $values($i)
|
||||||
|
}
|
||||||
|
return $str
|
||||||
|
}
|
||||||
|
method toStrF {{precision 7}} {
|
||||||
|
for {set i 0} {$i < [array size values]} {incr i} {
|
||||||
|
if {$i>0} {set str [string cat $str ", "]}
|
||||||
|
append str [my fformat $values($i) 10]
|
||||||
|
}
|
||||||
|
return $str
|
||||||
|
}
|
||||||
|
|
||||||
|
method initValue {i value} {set values($i) [expr double($value)] ; return [self]}
|
||||||
|
method initArray {a} {upvar $a arr ; for {set i 0} {$i < [array size arr]} {incr i} {my initValue $i $arr($i)} ; return [self]}
|
||||||
|
method initArrayLst {arr_lst} {array set arr $arr_lst ; for {set i 0} {$i < [array size arr]} {incr i} {my initValue $i $arr($i)} ; return [self]}
|
||||||
|
method initLst {lst} {for {set i 0} {$i < [llength $lst]} {incr i} {my initValue $i [lindex $lst $i]} ; return [self]}
|
||||||
|
|
||||||
|
method initMCS {mcs} {
|
||||||
|
my identity
|
||||||
|
set p [$mcs org] ; my initOrgP $p ; $p destroy
|
||||||
|
set v [$mcs xVct] ; my initXvctV $v ; $v destroy
|
||||||
|
set v [$mcs yVct] ; my initYvctV $v ; $v destroy
|
||||||
|
set v [$mcs zVct] ; my initZvctV $v ; $v destroy
|
||||||
|
return [self]
|
||||||
|
}
|
||||||
|
method initMTX {mtx} {
|
||||||
|
set p [$mtx org] ; my initOrg $p ; $p destroy
|
||||||
|
set v [$mtx xVct] ; my initXvct $v ; $v destroy
|
||||||
|
set v [$mtx yVct] ; my initYvct $v ; $v destroy
|
||||||
|
set v [$mtx zVct] ; my initZvct $v ; $v destroy
|
||||||
|
return [self]
|
||||||
|
}
|
||||||
|
|
||||||
|
method initOrg {dx dy dz} {my initValue 0 $dx ; my initValue 1 $dy ; my initValue 2 $dz ; return [self]}
|
||||||
|
method initOrgP {p} {return [my initOrg [$p x] [$p y] [$p z]]}
|
||||||
|
|
||||||
|
method initXvct {dx dy dz} {my initValue 3 $dx ; my initValue 4 $dy ; my initValue 5 $dz ; return [self]}
|
||||||
|
method initXvctV {v} {return [my initXvct [$v x] [$v y] [$v z]]}
|
||||||
|
|
||||||
|
method initYvct {dx dy dz} {my initValue 6 $dx ; my initValue 7 $dy ; my initValue 8 $dz ; return [self]}
|
||||||
|
method initYvctV {v} {return [my initYvct [$v x] [$v y] [$v z]]}
|
||||||
|
|
||||||
|
method initZvct {dx dy dz} {my initValue 9 $dx ; my initValue 10 $dy ; my initValue 11 $dz ; return [self]}
|
||||||
|
method initZvctV {v} {return [my initZvct [$v x] [$v y] [$v z]]}
|
||||||
|
|
||||||
|
method initOrgArray {a} {upvar $a arr ; return [my initOrg $arr(0) $arr(1) $arr(2)]}
|
||||||
|
method initVctsArray {a} {
|
||||||
|
upvar $a arr
|
||||||
|
my initXvct $arr(0) $arr(1) $arr(2)
|
||||||
|
my initYvct $arr(3) $arr(4) $arr(5)
|
||||||
|
my initZvct $arr(6) $arr(7) $arr(8)
|
||||||
|
return [self]
|
||||||
|
}
|
||||||
|
|
||||||
|
method value {i} {return $values($i)}
|
||||||
|
|
||||||
|
method org {args} {return [Point3D new $values(0) $values(1) $values(2)]}
|
||||||
|
method xVct {args} {return [Vector3D new $values(3) $values(4) $values(5)]}
|
||||||
|
method yVct {args} {return [Vector3D new $values(6) $values(7) $values(8)]}
|
||||||
|
method zVct {args} {return [Vector3D new $values(9) $values(10) $values(11)]}
|
||||||
|
|
||||||
|
method same {mcs {precision 7}} {
|
||||||
|
set org [[self] org] ; set org_1 [$mcs org]
|
||||||
|
set vx [[self] xVct] ; set vx_1 [$mcs xVct]
|
||||||
|
set vy [[self] yVct] ; set vy_1 [$mcs yVct]
|
||||||
|
set vz [[self] zVct] ; set vz_1 [$mcs zVct]
|
||||||
|
|
||||||
|
if {[$org same $org_1 $precision] && [$vx same $vx_1 $precision] && [$vy same $vy_1 $precision] && [$vz same $vz_1 $precision]} {
|
||||||
|
set value 1
|
||||||
|
} else {
|
||||||
|
set value 0
|
||||||
|
}
|
||||||
|
|
||||||
|
$org destroy ; $org_1 destroy
|
||||||
|
$vx destroy ; $vx_1 destroy
|
||||||
|
$vy destroy ; $vy_1 destroy
|
||||||
|
$vz destroy ; $vz_1 destroy
|
||||||
|
return $value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set point [Point3D new]
|
||||||
|
|
||||||
|
$point add 1 2 3
|
||||||
|
|
||||||
|
$custom_flag(from_move,$::mom_path_name)
|
||||||
|
|
||||||
|
oo::class create Point3D {
|
||||||
|
variable x 0. ; variable y 0. ; variable z 0.
|
||||||
|
|
||||||
|
constructor {{dx 0.} {dy 0.} {dz 0.}} {my initX $dx ; my initY $dy ; my initZ $dz}
|
||||||
|
destructor {
|
||||||
|
#puts "[self] is now deleted"
|
||||||
|
}
|
||||||
|
|
||||||
|
method fformat {value {precision 7}} {return [expr {round( 10.0 ** $precision * $value) / (10.0 ** $precision)}]}
|
||||||
|
method radtodeg {rad} {return [expr {$rad*180.0/(4.0*atan(1.0))}]}
|
||||||
|
method degtorad {deg} {return [expr {$deg*(4.0*atan(1.0))/180.0}]}
|
||||||
|
|
||||||
|
method duplicate {args} {return [[Point3D new] initP [self]]}
|
||||||
|
method type {args} {return "::Point3D"}
|
||||||
|
|
||||||
|
method toArray {{i 0}} {return [list [incr i 0] $x [incr i] $y [incr i] $z]}
|
||||||
|
method toLst {args} {return [list $x $y $z]}
|
||||||
|
method toStr {args} {return "$x, $y, $z"}
|
||||||
|
method toStrF {{precision 7}} {return "[my fformat $x $precision], [my fformat $y $precision], [my fformat $z $precision]"}
|
||||||
|
|
||||||
|
method x {args} {return $x}
|
||||||
|
method y {args} {return $y}
|
||||||
|
method z {args} {return $z}
|
||||||
|
|
||||||
|
method initX {dx} {set x [expr double($dx)]}
|
||||||
|
method initY {dy} {set y [expr double($dy)]}
|
||||||
|
method initZ {dz} {set z [expr double($dz)]}
|
||||||
|
|
||||||
|
method init {dx dy dz} {my initX $dx ; my initY $dy ; my initZ $dz ; return [self]}
|
||||||
|
method initArray {a} {upvar $a arr ; return [my init $arr(0) $arr(1) $arr(2)]}
|
||||||
|
method initArrayLst {arr_lst} {array set arr $arr_lst ; return [my init $arr(0) $arr(1) $arr(2)]}
|
||||||
|
method initLst {lst} {return [my init [lindex $lst 0] [lindex $lst 1] [lindex $lst 2]]}
|
||||||
|
method initP {p} {return [my init [$p x] [$p y] [$p z]]}
|
||||||
|
|
||||||
|
method reset {args} {my init 0. 0. 0. ; return [self]}
|
||||||
|
|
||||||
|
method add {dx dy dz} {my init [expr {$x + $dx}] [expr {$y + $dy}] [expr {$z + $dz}] ; return [self]}
|
||||||
|
method addArray {a} {upvar $a arr ; return [my add $arr(0) $arr(1) $arr(2)]}
|
||||||
|
method addArrayLst {arr_lst} {array set arr $arr_lst ; return [my add $arr(0) $arr(1) $arr(2)]}
|
||||||
|
method addLst {lst} {return [my add [lindex $lst 0] [lindex $lst 1] [lindex $lst 2]]}
|
||||||
|
method addP {p} {return [my add [$p x] [$p y] [$p z]]}
|
||||||
|
|
||||||
|
method sub {dx dy dz} {my init [expr {$x - $dx}] [expr {$y - $dy}] [expr {$z - $dz}] ; return [self]}
|
||||||
|
method subArray {a} {upvar $a arr ; return [my sub $arr(0) $arr(1) $arr(2)]}
|
||||||
|
method subArrayLst {arr_lst} {array set arr $arr_lst ; return [my sub $arr(0) $arr(1) $arr(2)]}
|
||||||
|
method subLst {lst} {return [my sub [lindex $lst 0] [lindex $lst 1] [lindex $lst 2]]}
|
||||||
|
method subP {p} {return [my sub [$p x] [$p y] [$p z]]}
|
||||||
|
|
||||||
|
method dist {dx dy dz} {return [expr {sqrt([expr {$x - $dx}]**2 + [expr {$y - $dy}]**2 + [expr {$z - $dz}]**2)}]}
|
||||||
|
method distArray {a} {upvar $a arr ; return [my dist $arr(0) $arr(1) $arr(2)]}
|
||||||
|
method distArrayLst {arr_lst} {array set arr $arr_lst ; return [my dist $arr(0) $arr(1) $arr(2)]}
|
||||||
|
method distLst {lst} {return [my dist [lindex $lst 0] [lindex $lst 1] [lindex $lst 2]]}
|
||||||
|
method distP {p} {return [my dist [$p x] [$p y] [$p z]]}
|
||||||
|
|
||||||
|
method dist_to_line {pl1 pl2} {
|
||||||
|
set pl [[$pl2 duplicate] subP $pl1]
|
||||||
|
set pp [[$pl1 duplicate] subP [self]]
|
||||||
|
|
||||||
|
set vl [[Vector3D new] initV $pl]
|
||||||
|
set vp [[Vector3D new] initV $pp]
|
||||||
|
|
||||||
|
set l [$vl magnitude]
|
||||||
|
|
||||||
|
if {[my fformat $l 5]==0} {
|
||||||
|
set value 0.
|
||||||
|
} else {
|
||||||
|
set vc [$vl cross $vp]
|
||||||
|
set value [expr [$vc magnitude]/$l]
|
||||||
|
$vc destroy
|
||||||
|
}
|
||||||
|
|
||||||
|
$pl destroy ; $vl destroy
|
||||||
|
$pp destroy ; $vp destroy
|
||||||
|
return $value
|
||||||
|
}
|
||||||
|
method dist_to_lineV {v} {
|
||||||
|
set p1 [[Point3D new] init 0. 0. 0.]
|
||||||
|
set p2 [[Point3D new] initP $v]
|
||||||
|
|
||||||
|
set value [my dist_to_line $p1 $p2]
|
||||||
|
$p1 destroy
|
||||||
|
$p2 destroy
|
||||||
|
return $value
|
||||||
|
}
|
||||||
|
|
||||||
|
#midpoint method to calculate the midpoint between this point and another point. This can be done by averaging the x, y, and z coordinates of the two points.
|
||||||
|
method midpoint {dx dy dz} {return [Point3D new [expr {double([[self] x] + $dx)/2}] [expr {double([[self] y] + $dy)/2}] [expr {double([[self] z] + $dz)/2}]]}
|
||||||
|
method midpointArr {a} {upvar $a arr ; return [my midpoint $arr(0) $arr(1) $arr(2)]}
|
||||||
|
method midpointArrLst {arr} {array set arr $arr_lst ; return [my midpoint $arr(0) $arr(1) $arr(2)]}
|
||||||
|
method midpointLst {lst} {return [my midpoint [lindex $lst 0] [lindex $lst 1] [lindex $lst 2]]}
|
||||||
|
method midpointP {p} {return [my midpoint [$p x] [$p y] [$p z]]}
|
||||||
|
|
||||||
|
method same {p {precision 7}} {
|
||||||
|
if {[my fformat [expr {[[self] x] - [$p x]}] $precision] == 0. && [my fformat [expr {[[self] y] - [$p y]}] $precision] == 0. && [my fformat [expr {[[self] z] - [$p z]}] $precision] == 0.} {return 1} else {return 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
#This method takes three arguments: a vector object "axis" as the axis of rotation, an angle of rotation, and a point object "center" as the center of rotation.
|
||||||
|
#First, it translates the point by subtracting the center of rotation,
|
||||||
|
#then it applies the Rodrigues' rotation formula to calculate the new coordinates of the point, and finally,
|
||||||
|
#it translates back the point by adding the center of rotation.
|
||||||
|
#The center parameter is defined as an optional parameter with a default value of a new Point3D object, initialized with the values (0,0,0).
|
||||||
|
method rotAround {u v w angle {x0 0.} {y0 0.} {z0 0.}} {
|
||||||
|
set x1 [expr {double($x) - $x0}] ; set y1 [expr {double($y) - $y0}] ; set z1 [expr {double($z) - $z0}]
|
||||||
|
set u [expr {double($u)}] ; set v [expr {double($v)}] ; set w [expr {double($w)}]
|
||||||
|
set a [my degtorad $angle]
|
||||||
|
|
||||||
|
set coss [expr {cos($a)}]
|
||||||
|
set sinn [expr {sin($a)}]
|
||||||
|
|
||||||
|
set x2 [expr {$u*($u*$x1 + $v*$y1 + $w*$z1)*(1 - $coss) + $x1*$coss + (-$w*$y1 + $v*$z1)*$sinn + $x0}]
|
||||||
|
set y2 [expr {$v*($u*$x1 + $v*$y1 + $w*$z1)*(1 - $coss) + $y1*$coss + ($w*$x1 - $u*$z1)*$sinn + $y0}]
|
||||||
|
set z2 [expr {$w*($u*$x1 + $v*$y1 + $w*$z1)*(1 - $coss) + $z1*$coss + (-$v*$x1 + $u*$y1)*$sinn + $z0}]
|
||||||
|
|
||||||
|
my init $x2 $y2 $z2
|
||||||
|
return [self]
|
||||||
|
}
|
||||||
|
method rotAroundLst {aLst angle {cLst {0. 0. 0.}}} {
|
||||||
|
return [my rotAround [lindex $aLst 0] [lindex $aLst 1] [lindex $aLst 2] $angle [lindex $cLst 0] [lindex $cLst 1] [lindex $cLst 2]]
|
||||||
|
}
|
||||||
|
method rotAroundVP {axis angle {center {}}} {
|
||||||
|
if {[catch {set s [$center type]}] || (![string equal [$center type] "::Point3D"] && ![string equal [$center type] "::Vector3D"])} {
|
||||||
|
set center [Point3D new]
|
||||||
|
set b_center true
|
||||||
|
}
|
||||||
|
|
||||||
|
my rotAround [$axis x] [$axis y] [$axis z] $angle [$center x] [$center y] [$center z]
|
||||||
|
if {[info exists b_center]} {$center destroy}
|
||||||
|
return [self]
|
||||||
|
}
|
||||||
|
|
||||||
|
method transform {mtx_from mtx_to} {
|
||||||
|
if {[catch {set s [$mtx_from type]}] || ![string equal [$mtx_from type] "::Matrix4x4"]} {
|
||||||
|
set mtx_from [[Matrix4x4 new] identity]
|
||||||
|
set b_mtx_from true
|
||||||
|
}
|
||||||
|
if {[catch {set s [$mtx_to type]}] || ![string equal [$mtx_to type] "::Matrix4x4"]} {
|
||||||
|
set mtx_to [[Matrix4x4 new] identity]
|
||||||
|
set b_mtx_to true
|
||||||
|
}
|
||||||
|
|
||||||
|
set m [[Matrix4x4 new] identity]
|
||||||
|
$m initValue 0 [my x]
|
||||||
|
$m initValue 4 [my y]
|
||||||
|
$m initValue 8 [my z]
|
||||||
|
$m initValue 12 1.
|
||||||
|
|
||||||
|
set trs [[[[$mtx_from duplicate] inverse] multiply $mtx_to false] inverse]
|
||||||
|
$m multiply $trs true
|
||||||
|
|
||||||
|
my init [$m value 0] [$m value 4] [$m value 8]
|
||||||
|
$m destroy
|
||||||
|
$trs destroy
|
||||||
|
if {[info exists b_mtx_from]} {$mtx_from destroy}
|
||||||
|
if {[info exists b_mtx_to]} {$mtx_to destroy}
|
||||||
|
return [self]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user