diff --git a/CHANGELOG.md b/CHANGELOG.md index 5073b4c..27ab576 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## 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 document highlights for procedure and variable occurrences - Make completion context-aware and prioritize local, current-file, workspace, and built-in symbols diff --git a/server/src/lsp_server.py b/server/src/lsp_server.py index 1cb4554..689b79b 100644 --- a/server/src/lsp_server.py +++ b/server/src/lsp_server.py @@ -75,6 +75,10 @@ from tools.semantic_tokens import ( _Highlighter, ) 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, @@ -278,6 +282,9 @@ def on_completion(params: lsp.CompletionParams) -> lsp.CompletionList: doc = LSP_SERVER.workspace.get_text_document(params.text_document.uri) position = params.position source_lines = LSP_SERVER.get_lines(doc) + oo_items = tcloo_completions(source_lines, position) + if oo_items is not None: + return lsp.CompletionList(is_incomplete=False, items=oo_items) array_items = array_element_completions( source_lines, position, LSP_SERVER.navigation_snapshot().values(), str(pathlib.Path(uris.to_fs_path(doc.uri))), @@ -334,7 +341,12 @@ def on_completion(params: lsp.CompletionParams) -> lsp.CompletionList: ): return lsp.CompletionList(is_incomplete=False, items=[]) - tree = LSP_SERVER.get_tree(doc) + 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( doc, tree ) @@ -347,6 +359,7 @@ def on_completion(params: lsp.CompletionParams) -> lsp.CompletionList: break 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): candidates.append( ( @@ -457,7 +470,15 @@ def on_completion(params: lsp.CompletionParams) -> lsp.CompletionList: ) def signature_help(params: lsp.SignatureHelpParams) -> lsp.SignatureHelp | None: 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( document.path @@ -541,6 +562,7 @@ def semantic_tokens(params: lsp.SemanticTokensParams): # Reuse cached AST tree = LSP_SERVER.get_tree(document) + hl.highlight_classes(tree) tree.accept(hl, recurse=True) tokens = hl.tokens() diff --git a/server/src/lsp_tclserver.py b/server/src/lsp_tclserver.py index 165e8dc..77ff4f0 100644 --- a/server/src/lsp_tclserver.py +++ b/server/src/lsp_tclserver.py @@ -14,6 +14,7 @@ from tclint.lexer import TclSyntaxError from tclint.violations import Violation from tools import checks, parser from tools.completion_items import CompletionCollector +from tools.tcloo_symbols import class_completion_items from tools.formatter import NxFormatter as Formatter from tools.inlay_hint import InlayHintSignature, build_custom_inlay_signatures from tools.navigation import FileSymbolIndex, build_file_symbol_index @@ -190,6 +191,7 @@ class TclLanguageServer(LanguageServer): item.label for path_items in self.poco_completion.values() for item in path_items + if item.kind != lsp.CompletionItemKind.Class ) self._custom_function_names_cache = (self._index_generation, names) return names @@ -542,6 +544,7 @@ class TclLanguageServer(LanguageServer): else self.parse_source(document.source) ) tree.accept(collector, recurse=True) + collector.custom_functions.extend(class_completion_items(tree)) docs = build_proc_docs(tree, document.source) navigation_index = build_file_symbol_index( filepath, document.uri, tree diff --git a/server/src/tools/completion_items.py b/server/src/tools/completion_items.py index 86042b3..bf54877 100644 --- a/server/src/tools/completion_items.py +++ b/server/src/tools/completion_items.py @@ -33,6 +33,7 @@ VARIABLE_KINDS = { lsp.CompletionItemKind.Constant, } COMMAND_KINDS = { + lsp.CompletionItemKind.Class, lsp.CompletionItemKind.Function, lsp.CompletionItemKind.Method, lsp.CompletionItemKind.Constructor, diff --git a/server/src/tools/inlay_hint.py b/server/src/tools/inlay_hint.py index fea5368..40e8091 100644 --- a/server/src/tools/inlay_hint.py +++ b/server/src/tools/inlay_hint.py @@ -9,6 +9,8 @@ from typing import Any import lsprotocol.types as lsp from tclint.syntax_tree import Command, VarSub, Visitor from tools.navigation import FileSymbolIndex +from tools.tcloo_arguments import method_parameters +from tools.tcloo_completion import resolved_method_calls @dataclass(frozen=True) @@ -160,6 +162,7 @@ class InlayHintGenerator(Visitor): self.source_lines = ( source_lines if source_lines is not None else source.splitlines() ) + self.source = source self.proc_signatures = proc_signatures self.requested_range = requested_range self.parameter_names = parameter_names @@ -194,6 +197,17 @@ class InlayHintGenerator(Visitor): walk(child) 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 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": 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) if parameter is None: break diff --git a/server/src/tools/semantic_tokens.py b/server/src/tools/semantic_tokens.py index ff58796..f2163e0 100644 --- a/server/src/tools/semantic_tokens.py +++ b/server/src/tools/semantic_tokens.py @@ -6,6 +6,7 @@ from common.load_data import standard_items from tclint.commands.plugins import PluginManager 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 # the builtin command set on every call. Semantic tokens are requested often, so @@ -70,6 +71,7 @@ class _Highlighter(Visitor): def __init__(self, plugins, custom_functions): self._commands = _load_commands(plugins) self._tokens = [] + self._class_tokens = {} if isinstance(custom_functions, dict): self._custom_function_names = frozenset( item.label @@ -84,6 +86,17 @@ class _Highlighter(Visitor): return 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): """Hilfsmethode um Token-Informationen aus verschiedenen Node-Typen zu extrahieren.""" if not hasattr(node, "pos"): @@ -204,7 +217,9 @@ class _Highlighter(Visitor): tokens = [] last_line = 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 col_delta = col if line == last_line: diff --git a/server/src/tools/tcloo_arguments.py b/server/src/tools/tcloo_arguments.py new file mode 100644 index 0000000..d9c32e9 --- /dev/null +++ b/server/src/tools/tcloo_arguments.py @@ -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, + ) diff --git a/server/src/tools/tcloo_completion.py b/server/src/tools/tcloo_completion.py new file mode 100644 index 0000000..fb1f3f2 --- /dev/null +++ b/server/src/tools/tcloo_completion.py @@ -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 [] diff --git a/server/src/tools/tcloo_symbols.py b/server/src/tools/tcloo_symbols.py new file mode 100644 index 0000000..4a3638b --- /dev/null +++ b/server/src/tools/tcloo_symbols.py @@ -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)] diff --git a/server/tests/python_tests/test_tcloo_arguments.py b/server/tests/python_tests/test_tcloo_arguments.py new file mode 100644 index 0000000..f9b269b --- /dev/null +++ b/server/tests/python_tests/test_tcloo_arguments.py @@ -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" diff --git a/server/tests/python_tests/test_tcloo_completion.py b/server/tests/python_tests/test_tcloo_completion.py new file mode 100644 index 0000000..61e5eb0 --- /dev/null +++ b/server/tests/python_tests/test_tcloo_completion.py @@ -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" diff --git a/test/test.tcl b/test/test.tcl index eb9f333..bfba383 100644 --- a/test/test.tcl +++ b/test/test.tcl @@ -6,24 +6,24 @@ set te875st 11111 #set ::custom_flag(from_move,$::mom_path_name) 1 if {$main == 1 && 1 == 1} { - puts "main" + puts "main" } proc test {} { - puts "main" - proc llll {} {} - set rrrrrrr + puts "main" + proc llll {} {} + 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 MOM_abort namespace eval myns { - proc add {a b} { - set sum [expr {$a + $b}] - return $sum - } - set config "debug" + proc add {a b} { + set sum [expr {$a + $b}] + return $sum + } + set config "debug" } #_________________________________________________________________________________________________ @@ -31,7 +31,7 @@ namespace eval myns { # Function to output a spacer line or empty line #_________________________________________________________________________________________________ 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 #_________________________________________________________________________________________________ proc SERVICE_remove_file {file} { - if {![SERVICE_check_file_exists $file]} {return} - MOM_remove_file $file + if {![SERVICE_check_file_exists $file]} {return} + MOM_remove_file $file } #_________________________________________________________________________________________________ @@ -51,8 +51,8 @@ proc SERVICE_remove_file {file} { # Function to check if the file exists #_________________________________________________________________________________________________ proc SERVICE_check_file_exists {file} { - if {[file exists $file]} {return 1} - return 0 + if {[file exists $file]} {return 1} + return 0 } #_________________________________________________________________________________________________ @@ -60,19 +60,19 @@ proc SERVICE_check_file_exists {file} { # Ask UDE Info for the Tool #_________________________________________________________________________________________________ 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} { - return 1 - } - return 0 + if {[lsearch $::mom_result $ude_name] != -1} { + return 1 + } + return 0 } proc MOM_dummy_event_start {} { - global mom_new_item - global mom_new_item_end + global mom_new_item + 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 #_________________________________________________________________________________________________ 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} { - return 1 - } - return 0 + if {[lsearch $::mom_result $ude_name] != -1} { + return 1 + } + return 0 } #_________________________________________________________________________________________________ @@ -96,7 +96,7 @@ proc SERVICE_ask_ude_operation {pos ude_name path_name} { # restore #_________________________________________________________________________________________________ proc SERVICE_output_handling {handler} { - set ::lib_ge(hidden_output) $handler + set ::lib_ge(hidden_output) $handler } #_________________________________________________________________________________________________ @@ -105,21 +105,301 @@ proc SERVICE_output_handling {handler} { # this function is called in start of program #_________________________________________________________________________________________________ proc SERVICE_get_tool_data {} { - global mom_tool_data - global mom_operation_info + global mom_tool_data + global mom_operation_info - set mom_tool_data(toollist) "" - set operations $::mom_operation_name_list - foreach operation $operations { - 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) - } - } + set mom_tool_data(toollist) "" + set operations $::mom_operation_name_list + foreach operation $operations { + 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) + } + } } 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 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] + } +}