Compare commits

...
17 Commits
Author SHA1 Message Date
Christoph 91b86f29cb Merge pull request 'Add TclOO class completions, signature help and inlay hints' (#39) from add_class_support into main
build_and_puplish.yml / build_and_publish (release) Successful in 39s
2026-09-11 21:39:34 +00:00
Christoph Brandau f88a50d4ab feat(tcloo): add document-local TclOO completions, signature help and hints
Add conservative, document-local TclOO type inference and tooling so the server
can offer method completions, signature help, and inlay parameter hints for
statically resolvable TclOO receivers (including `new`/`create`, `my`, and
simple return-chains). Also surface class names as completion items and emit
semantic tokens for class declarations/references.

Notable changes:
- New tcloo_* tools: completion, symbols, and argument parsing; integrated into
  on_completion, signature_help, inlay hint generation and semantic token
  highlighting. Completions are returned early when an OO receiver context is
  detected.
- Use a completion-friendly parser fallback when the main AST fails (TclSyntaxError)
  so editing-in-progress code still yields useful completions.
- Add CompletionItemKind.Class to command kinds, exclude class items from the
  poco completion name cache, and include new unit tests for the TclOO helpers.
2026-09-11 23:38:42 +02:00
Christoph c28836933c Update version to 2026.9.300 2026-09-10 08:31:25 +00:00
Christoph 91c8c4aff3 Merge pull request 'Check Array Paramter' (#38) from #150 into main
build_and_puplish.yml / build_and_publish (release) Successful in 34s
2026-09-10 08:29:22 +00:00
Christoph Brandau d8611d7aea feat(tcl): add static variable name extraction and array key completion
This adds tooling to statically extract Tcl variable names from syntax
trees without evaluating substitutions, enabling better completions
for array elements and plain variables. The new helpers are wired
into the completion and navigation flows and are supported by tests
covering array keys and substitutions.

- Introduce variable_names.py with variable_name() and array_key_parts()
- Wire static name extraction into completion and symbol indexing
- Add tests for array key completion with substitutions
2026-09-10 09:07:18 +02:00
Christoph Brandau 7e9a4359cd feat(cdl): include args in generated event handler snippet
The generated event handler snippet now declares the proc with an args block.
This enables parameters to be referenced within the handler body.
The change makes the snippet compatible with events that pass arguments.

- Include an args block in the proc declaration for event handlers.
2026-09-10 08:45:25 +02:00
Christoph b7f28ab1a3 Update version to 2026.9.220 2026-09-08 19:22:31 +00:00
Christoph 9b368ba761 Merge pull request 'fix(completion): scan nested commands in unfinished braced args' (#37) from #36 into main
build_and_puplish.yml / build_and_publish (release) Successful in 34s
Reviewed-on: #37
2026-09-08 19:21:49 +00:00
Christoph 82e13cf7ce fix(completion): scan nested commands in unfinished braced args
Make the completion parser inspect nested command segments when an
unfinished braced argument is the active context, instead of treating the
entire braced body as opaque. This resolves cases where inner commands
(e.g. command substitutions) were ignored and improves suggestion
accuracy. Tests were added to cover nested commands inside braced
conditions and to ensure closed braced arguments do not change context.
Also add Linux-specific launch and task configurations to IDE settings
to simplify local extension development and debugging.

- Recursively scan inner context when a braced body is unfinished
- Add tests for nested commands and closed-brace behavior
- Add Linux launch/task entries for easier development
2026-09-08 21:16:58 +02:00
Christoph d104906508 Update version to 2026.9.210 2026-09-04 11:46:55 +00:00
Christoph a5ff3b55ff Merge pull request 'feat(syntax): add escaped-quoted-strings token to def syntax' (#35) from new_completions into main
build_and_puplish.yml / build_and_publish (release) Successful in 7m35s
Reviewed-on: #35
2026-09-04 11:39:08 +00:00
Christoph Brandau 26225eb8ed feat(syntax): add escaped-quoted-strings token to def syntax
Introduces an escaped-quoted-strings pattern for the def syntax.
It enables proper highlighting of strings with escapes and vars.
The change adds the new pattern to the syntax and fixes the EOF newline.

- Adds escaped-quoted-strings for escapes and embedded vars
- Registers new pattern in repository for proper highlighting
- Ensures trailing newline in the file
2026-09-04 08:39:46 +02:00
Christoph 844138b383 Update version to 2026.9.200
build_and_puplish.yml / build_and_publish (release) Canceled after 33s
2026-09-03 08:39:22 +00:00
Christoph d7eb72f417 Merge pull request 'New completions' (#34) from new_completions into main
build_and_puplish.yml / build_and_publish (release) Successful in 27s
Reviewed-on: #34
2026-09-03 08:36:00 +00:00
Christoph Brandau af5acfc946 feat(tcl): add dynamic argument completion and snippets
The changes add dynamic, semantic argument completion for Tcl
commands and snippet support.

- Introduces DynamicCompletionKind, TclArgumentCompletion, and dynamic rules
  for Tcl to provide variable, procedure, namespace, and path suggestions.
- Adds snippet-backed commands and arguments for Tcl blocks and paths.
- Refactors tcl_argument_completion and updates the LSP to use dynamic path,
  variable, and namespace completions with snippet kinds.
2026-09-03 10:35:39 +02:00
Christoph Brandau 20f76b6a20 feat(lsp): add Tcl command-aware completion and integration
- Introduced a Tcl command-aware completion engine and wired to LSP.
- Added a new module with Tcl commands, options, and contextual matching.
- Updated completion to favor command-aware items and args.
2026-09-03 10:17:16 +02:00
Christoph 081b488fe3 Update version to 2026.9.100 2026-09-03 07:38:12 +00:00
23 changed files with 2991 additions and 89 deletions
+26
View File
@@ -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",
+7
View File
@@ -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",
+6
View File
@@ -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
+4 -2
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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": [
+174 -11
View File
@@ -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)
tree = LSP_SERVER.get_tree(doc) 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)
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)
tree = LSP_SERVER.get_tree(document) 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)
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()
+3
View File
@@ -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
+101 -18
View File
@@ -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
+18 -1
View File
@@ -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
+15 -2
View File
@@ -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,
+22 -1
View File
@@ -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
+79
View File
@@ -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,
)
+226
View File
@@ -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 []
+53
View File
@@ -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)]
+20 -9
View File
@@ -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("::"):
+44
View File
@@ -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"
+28
View File
@@ -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": [
{ {
+318 -39
View File
@@ -6,24 +6,24 @@ set te875st 11111
#set ::custom_flag(from_move,$::mom_path_name) 1 #set ::custom_flag(from_move,$::mom_path_name) 1
if {$main == 1 && 1 == 1} { if {$main == 1 && 1 == 1} {
puts "main" puts "main"
} }
proc test {} { proc test {} {
puts "main" puts "main"
proc llll {} {} proc llll {} {}
set rrrrrrr set rrrrrrr
} }
LIB_GE_command_buffer_edit_insert MOM_tool_change_LIB TOOL_CHANGE_AUTO {CUSTOM_after_tool_change_call} mytag after @TOOL_CHANGE_AUTO LIB_GE_command_buffer_edit_insert MOM_tool_change_LIB TOOL_CHANGE_AUTO {CUSTOM_after_tool_change_call} mytag after @TOOL_CHANGE_AUTO
MOM_abort MOM_abort
namespace eval myns { namespace eval myns {
proc add {a b} { proc add {a b} {
set sum [expr {$a + $b}] set sum [expr {$a + $b}]
return $sum return $sum
} }
set config "debug" set config "debug"
} }
#_________________________________________________________________________________________________ #_________________________________________________________________________________________________
@@ -31,7 +31,7 @@ namespace eval myns {
# Function to output a spacer line or empty line # Function to output a spacer line or empty line
#_________________________________________________________________________________________________ #_________________________________________________________________________________________________
proc SERVICE_spacer_output {type {length 20} {line_num 0} {output 1}} { proc SERVICE_spacer_output {type {length 20} {line_num 0} {output 1}} {
LIB_GE_message [string repeat $type $length] "output_$output" $line_num LIB_GE_message [string repeat $type $length] "output_$output" $line_num
} }
@@ -42,8 +42,8 @@ SERVICE_spacer_output "*" 2 0 0
# Function to delete the file # Function to delete the file
#_________________________________________________________________________________________________ #_________________________________________________________________________________________________
proc SERVICE_remove_file {file} { proc SERVICE_remove_file {file} {
if {![SERVICE_check_file_exists $file]} {return} if {![SERVICE_check_file_exists $file]} {return}
MOM_remove_file $file MOM_remove_file $file
} }
#_________________________________________________________________________________________________ #_________________________________________________________________________________________________
@@ -51,8 +51,8 @@ proc SERVICE_remove_file {file} {
# Function to check if the file exists # Function to check if the file exists
#_________________________________________________________________________________________________ #_________________________________________________________________________________________________
proc SERVICE_check_file_exists {file} { proc SERVICE_check_file_exists {file} {
if {[file exists $file]} {return 1} if {[file exists $file]} {return 1}
return 0 return 0
} }
#_________________________________________________________________________________________________ #_________________________________________________________________________________________________
@@ -60,19 +60,19 @@ proc SERVICE_check_file_exists {file} {
# Ask UDE Info for the Tool # Ask UDE Info for the Tool
#_________________________________________________________________________________________________ #_________________________________________________________________________________________________
proc SERVICE_ask_ude_tool {pos ude_name tool_name} { proc SERVICE_ask_ude_tool {pos ude_name tool_name} {
MOM_ask_ude_info $tool_name "tool" $pos MOM_ask_ude_info $tool_name "tool" $pos
if {[lsearch $::mom_result $ude_name] != -1} { if {[lsearch $::mom_result $ude_name] != -1} {
return 1 return 1
} }
return 0 return 0
} }
proc MOM_dummy_event_start {} { proc MOM_dummy_event_start {} {
global mom_new_item global mom_new_item
global mom_new_item_end global mom_new_item_end
#Put your UDE Handler Tcl here #Put your UDE Handler Tcl here
} }
#_________________________________________________________________________________________________ #_________________________________________________________________________________________________
@@ -80,12 +80,12 @@ proc MOM_dummy_event_start {} {
# Ask UDE Info for the Operation # Ask UDE Info for the Operation
#_________________________________________________________________________________________________ #_________________________________________________________________________________________________
proc SERVICE_ask_ude_operation {pos ude_name path_name} { proc SERVICE_ask_ude_operation {pos ude_name path_name} {
MOM_ask_ude_info $path_name "operation" $pos MOM_ask_ude_info $path_name "operation" $pos
if {[lsearch $::mom_result $ude_name] != -1} { if {[lsearch $::mom_result $ude_name] != -1} {
return 1 return 1
} }
return 0 return 0
} }
#_________________________________________________________________________________________________ #_________________________________________________________________________________________________
@@ -96,7 +96,7 @@ proc SERVICE_ask_ude_operation {pos ude_name path_name} {
# restore # restore
#_________________________________________________________________________________________________ #_________________________________________________________________________________________________
proc SERVICE_output_handling {handler} { proc SERVICE_output_handling {handler} {
set ::lib_ge(hidden_output) $handler set ::lib_ge(hidden_output) $handler
} }
#_________________________________________________________________________________________________ #_________________________________________________________________________________________________
@@ -105,22 +105,301 @@ proc SERVICE_output_handling {handler} {
# this function is called in start of program # this function is called in start of program
#_________________________________________________________________________________________________ #_________________________________________________________________________________________________
proc SERVICE_get_tool_data {} { proc SERVICE_get_tool_data {} {
global mom_tool_data global mom_tool_data
global mom_operation_info global mom_operation_info
set mom_tool_data(toollist) "" set mom_tool_data(toollist) ""
set operations $::mom_operation_name_list set operations $::mom_operation_name_list
foreach operation $operations { foreach operation $operations {
if {[lsearch -exact $mom_tool_data(toollist) $mom_operation_info($operation,tool_name)] == -1} { if {[lsearch -exact $mom_tool_data(toollist) $mom_operation_info($operation,tool_name)] == -1} {
lappend mom_tool_data(toollist) $mom_operation_info($operation,tool_name) lappend mom_tool_data(toollist) $mom_operation_info($operation,tool_name)
} }
} }
} }
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]
}
}