update lsp_server

This commit is contained in:
Christoph Brandau
2025-07-31 16:19:09 +02:00
parent 44fd772151
commit 98e2d104a8
8 changed files with 299 additions and 200 deletions
+25
View File
@@ -514,6 +514,31 @@
"set syslog [MOM_ask_syslog_name]"
]
},
{
"label": "MOM_ask_ude_info",
"kind": "function",
"description": "This command is used to retrieve the information about a user-defined event (UDE) of a specified object.",
"format": "MOM_ask_ude_info object_name object_type <Start/End/\"\">",
"parameters": [
{
"name": "Object_name",
"desc": "The name of the object to which the UDE is attached. It can be a group, an operation, a tool, a geometry, or a method."
},
{
"name": "object_type",
"desc": "The type of the object to which the UDE is attached. The following types are available.(group || operation/oper || tool || geometry/geom || method/meth)"
},
{
"name": "Start/End/\"\"",
"desc": "This parameter is optional. Indicates whether you want to retrieve the UDE attached to the start event, end event, or both. If it is empty, returns the UDEs attached to the specified operation."
}
],
"returns": [
"0 - The UDE information retrieval failed.",
"1 - The UDE information successfully retrieved."
],
"example": []
},
{
"label": "MOM_cancel_suppress_force_once_per_event",
"kind": "function",
+6 -6
View File
@@ -54,7 +54,7 @@ from tclint.violations import Violation
from plugins.poco_plugin import commands
from tools import checks, parser
from tools.semantic_tokens import _Highlighter, TOKEN_TYPES, TokenModifier
from tools.completion_items import _Completion
from tools.completion_items import completion
DIAGNOSTIC_SOURCE = "nx-post-support"
@@ -62,7 +62,7 @@ DIAGNOSTIC_SOURCE = "nx-post-support"
class TclLanguageServer(server.LanguageServer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.parser = parser.CustomParser() # Parser()
self.parser = Parser()
for command in commands:
self.parser._commands.update(command)
self.diagnostics = {}
@@ -215,10 +215,10 @@ def did_change(params: lsp.DidChangeTextDocumentParams) -> None:
"""LSP handler for textDocument/didChange request"""
document = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
LSP_SERVER.compute_diagnostics(document)
ci = _Completion()
completion.reset()
tree = LSP_SERVER.parser.parse(document.source)
tree.accept(ci, recurse=True)
LSP_SERVER._custom_functions = ci.custom_functions
tree.accept(completion, recurse=True)
log_to_output(tree.pretty(2))
@LSP_SERVER.feature(
@@ -254,7 +254,7 @@ def on_completion(params: lsp.CompletionParams) -> list[lsp.CompletionItem]:
standard_items.tcl_keyword_list
+ standard_items.nx_procs
+ standard_items.nx_variables
+ LSP_SERVER._custom_functions
+ completion.custom_functions
)
return lsp.CompletionList(is_incomplete=False, items=items)
+54 -1
View File
@@ -1,4 +1,57 @@
from enum import Enum
from tclint.syntax_tree import Visitor, BareWord, Command
from tclint.violations import Violation, Rule
class Rules(Enum):
VALIDATION = "validation"
OPTIONAL_ARG_POSITION = "optinal_args"
def __str__(self):
return self.value
class CommandArgsCheck(Visitor):
def __init__(self):
self._violations = []
def check(self, _, tree):
self._violations.clear()
tree.accept(self, recurse=True)
return self._violations
def visit_command(self, command: Command):
if (
not hasattr(command.routine, "contents")
or command.routine.contents != "proc"
):
return
if len(command.args) < 2:
return
args_node = command.args[1]
if not hasattr(args_node, "children"):
return
found_optional = False
for arg in args_node.children:
# Required argument
if isinstance(arg, BareWord):
if found_optional:
self._violations.append(
Violation(
Rules.OPTIONAL_ARG_POSITION,
"Required argument follows optional one",
arg.pos,
arg.end_pos,
)
)
# Optional argument
elif hasattr(arg, "children") and len(arg.children) >= 2:
found_optional = True
def get_checkers():
checkers = ()
checkers = (CommandArgsCheck(),)
return checkers
+6
View File
@@ -24,6 +24,9 @@ class _Completion(Visitor):
def custom_functions(self) -> list[lsp.CompletionItem]:
return self._custom_functions
def reset(self):
self._custom_functions = []
def visit_command(self, command):
routine = command.routine
@@ -37,3 +40,6 @@ class _Completion(Visitor):
label=first_arg.value, kind=lsp.CompletionItemKind.Function
)
)
completion = _Completion()
+62 -153
View File
@@ -1,12 +1,4 @@
from tclint.parser import (
Parser,
_is_bool_literal,
_is_float_literal,
_is_float_prefix,
_is_function,
_is_int_literal,
_is_int_prefix,
)
from tclint.parser import Parser, _strip_ws
from tclint.lexer import (
STATE_BRACEDWORD,
TOK_BACKSLASH_NEWLINE,
@@ -35,29 +27,21 @@ from tclint.syntax_tree import (
class CustomParser(Parser):
def _strip_ws(parse_func):
"""Decorator used by expression parser for stripping whitespace around a node."""
def func(parser, ts):
while ts.type() in {TOK_WS, TOK_BACKSLASH_NEWLINE, TOK_NEWLINE}:
ts.next()
node = parse_func(parser, ts)
while ts.type() in {TOK_WS, TOK_BACKSLASH_NEWLINE, TOK_NEWLINE}:
ts.next()
return node
return func
@_strip_ws
def _parse_expression(self, ts):
op1 = self._parse_operand(ts)
expr = op1
# Add TOK_BACKSLASH_NEWLINE to the tokens we need to skip
while ts.type() == TOK_BACKSLASH_NEWLINE:
ts.next()
# last condition is hack to break out of expression in case we're in ternary op
if ts.type() not in {TOK_EOF, TOK_RPAREN} and ts.value() not in {":", ","}:
if ts.type() not in {
TOK_EOF,
TOK_RPAREN,
TOK_BACKSLASH_NEWLINE,
} and ts.value() not in {":", ","}:
if ts.value() == "?":
# weird hack to record operator
start = ts.pos()
@@ -84,142 +68,67 @@ class CustomParser(Parser):
)
else:
operator = self._parse_operator(ts)
while ts.type() == TOK_BACKSLASH_NEWLINE:
ts.next()
op2 = self._parse_expression(ts)
expr = BinaryOp(op1, operator, op2, pos=op1.pos, end_pos=op2.end_pos)
if ts.type() != TOK_RPAREN and ts.value() not in {":", ","}:
if ts.type() not in (
TOK_RPAREN,
TOK_BACKSLASH_NEWLINE,
) and ts.value() not in {":", ","}:
ts.expect(TOK_EOF, message="expected end of expression", pos=ts.pos())
return expr
@_strip_ws
def _parse_operand(self, ts):
if ts.type() == TOK_DOLLAR:
return self.parse_var_sub(ts)
if ts.type() == TOK_QUOTE:
return self.parse_quoted_word(ts)
if ts.type() == TOK_LBRACE:
return self.parse_braced_word(ts)
if ts.type() == TOK_LBRACKET:
return self.parse_command_sub(ts)
if ts.type() == TOK_LPAREN:
start = ts.pos()
ts.next()
expr = self._parse_expression(ts)
ts.expect(
TOK_RPAREN,
message="reached EOF without finding match for paren",
pos=expr.pos,
)
end = ts.pos()
return ParenExpression(expr, start, end)
if ts.value() in {"-", "+", "~", "!"}:
operator_val = ts.value()
operator_pos = ts.pos()
ts.next()
operator = BareWord(operator_val, pos=operator_pos, end_pos=ts.pos())
operand = self._parse_operand(ts)
# Since _parse_operand() munches whitespace after the operand, we
# set the end of the UnaryOp to the end of the operand rather than
# ts.pos(). Otherwise, the bounds of the UnaryOp would include all
# that whitespace.
return UnaryOp(operator, operand, pos=operator_pos, end_pos=operand.end_pos)
# If none of these, collect tokens that may comprise an operand
operand = ""
operand_pos = ts.pos()
# First, we want to check for numeric operands (either ints or numeric
# floats) by consuming tokens as long as they comprise the prefix of a
# numeric operand
while ts.type() != TOK_EOF and (
_is_int_prefix(operand + ts.value())
or _is_float_prefix(operand + ts.value())
):
operand += ts.value()
ts.next()
# Next, we check if we've consumed an entire numeric literal. If so, we
# move on. If not, we keep consuming tokens that may correspond to a
# valid bareword (pretty much just alphanumeric chars).
if not (_is_int_literal(operand) or _is_float_literal(operand)):
while ts.type() in {TOK_ALPHA_CHARS, TOK_NUM_CHARS}:
operand += ts.value()
ts.next()
# The above method is a little hacky. Note that it doesn't parse things
# exactly the same as Tcl. E.g. if a script includes `expr {1foo}`,
# tclint will report an invalid operator "foo", whereas tclsh will
# report an invalid bareword "1foo". Despite reporting them differently
# both tools should still catch the same syntax errors, since there are
# no legal barewords that begin with a numeric literal prefix, and tclsh
# will stop parsing numeric operands if they're actually followed by a
# legal operator (e.g. `expr {1eq1}` will be handled properly).
is_func = _is_function(operand)
if not (
_is_int_literal(operand)
or _is_float_literal(operand)
or _is_bool_literal(operand)
or is_func
):
raise TclSyntaxError(
f"invalid bareword in expression: {operand}", operand_pos, ts.pos()
)
node = BareWord(operand, pos=operand_pos, end_pos=ts.pos())
if is_func:
node = self._parse_function(ts, node)
return node
def parse_braced_word(self, ts):
self.debug(f"parse_braced_word({ts.current})")
def _parse_operator(self, ts):
pos = ts.pos()
ts.lexer.push_state(STATE_BRACEDWORD)
# hacky logic to handle parsing legal operators
ts.assert_(TOK_LBRACE)
word = ""
expected_braces = [pos] # Stack für geschachtelte Klammern
while True:
toktype = ts.type()
if toktype == TOK_EOF:
raise TclSyntaxError(
"reached EOF without finding match for brace",
expected_braces[-1],
ts.pos(),
)
if toktype == TOK_BACKSLASH_NEWLINE:
# TCL-spezifisch: Zeilenumbruch mit Backslash ignorieren
ts.next()
continue
if toktype == TOK_LBRACE:
expected_braces.append(ts.pos())
elif toktype == TOK_RBRACE:
try:
expected_braces.pop()
except IndexError:
start = ts.pos()
ts.next()
end = ts.pos()
raise TclSyntaxError(
"found closing brace without matching open brace", start, end
)
if len(expected_braces) == 0:
ts.lexer.pop_state()
ts.next()
break
word += ts.value()
# Skip any backslash-newlines before the operator
while ts.type() in {TOK_WS, TOK_BACKSLASH_NEWLINE, TOK_NEWLINE}:
ts.next()
end_pos = ts.pos()
return BracedWord(word, pos=pos, end_pos=end_pos)
if ts.value() in {"&&", "and"}: # Add explicit handling for logical AND
operator = ts.value()
ts.next()
return BareWord(operator, pos=pos, end_pos=ts.pos())
elif ts.value() in {"*", "&", "|"}:
# one or two of these characters are legal operators
operator = ts.value()
ts.next()
if ts.value() == operator:
operator += ts.value()
ts.next()
elif ts.value() in {"<", ">"}:
operator = ts.value()
ts.next()
if ts.value() in {operator, "="}:
operator += ts.value()
ts.next()
elif ts.value() in {"=", "!"}:
operator = ts.value()
ts.next()
if ts.value() != "=":
raise TclSyntaxError(
f"invalid operator in expression: {operator}", pos, ts.pos()
)
operator += ts.value()
ts.next()
elif ts.value() in {"*", "/", "%", "+", "-", "^", "eq", "ne", "in", "ni"}:
operator = ts.value()
ts.next()
else:
while ts.type() in {TOK_WS, TOK_BACKSLASH_NEWLINE, TOK_NEWLINE}:
ts.next()
if ts.value() in {"&&", "and"}: # Try again after whitespace
operator = ts.value()
ts.next()
else:
raise TclSyntaxError(
f"invalid operator in expression: {ts.value()}", pos, ts.pos()
)
return BareWord(operator, pos=pos, end_pos=ts.pos())
+52 -35
View File
@@ -1,8 +1,10 @@
import enum
from typing import List
from tclint.syntax_tree import Visitor
from tclint.syntax_tree import Visitor, QuotedWord, Command, BareWord
from tclint.commands import get_commands
import attrs
from common.load_data import standard_items
from tools.completion_items import completion
class TokenModifier(enum.IntFlag):
@@ -10,6 +12,7 @@ class TokenModifier(enum.IntFlag):
readonly = enum.auto()
defaultLibrary = enum.auto()
definition = enum.auto()
declaration = enum.auto()
@attrs.define
@@ -30,6 +33,8 @@ TOKEN_TYPES = [
"parameter",
"type",
"class",
"string",
"parameter",
]
@@ -39,26 +44,52 @@ class _Highlighter(Visitor):
self._tokens = []
self.log_to_output = log_to_output
def visit_command(self, command):
def visit_quoted_word(self, word: QuotedWord):
if not word.contents:
return
line, col = word.contents_pos
self._tokens.append(((line - 1, col - 1), len(word.contents), "string", []))
pass
def visit_bare_word(self, word: BareWord):
if any(item.label == word.value for item in standard_items.nx_procs) or any(
item.label == word.value for item in completion.custom_functions
):
line, col = word.pos
self._tokens.append(
(((line - 1, col - 1), len(word.value), "function", []))
)
def visit_command(self, command: Command):
routine = command.routine
self.log_to_output(str(command.routine))
if routine.contents in self._commands:
line, col = routine.pos
self._tokens.append(((line - 1, col - 1), len(routine.contents), "keyword"))
if routine.contents == "set" and command.args:
first_arg = command.args[0]
if hasattr(first_arg, "pos") and hasattr(first_arg, "value"):
line, col = first_arg.pos
self._tokens.append(
(((line - 1, col - 1), len(first_arg.value), "variable"))
(
(
(line - 1, col - 1),
len(first_arg.value),
"variable",
[TokenModifier.declaration],
)
)
)
if routine.contents == "proc" and command.args:
first_arg = command.args[0]
if hasattr(first_arg, "pos") and hasattr(first_arg, "value"):
line, col = first_arg.pos
self._tokens.append(
(((line - 1, col - 1), len(first_arg.value), "function"))
(
(
(line - 1, col - 1),
len(first_arg.value),
"function",
[TokenModifier.declaration],
)
)
)
if len(command.args) >= 2:
@@ -71,7 +102,12 @@ class _Highlighter(Visitor):
if hasattr(child, "value") and child.value is not None:
line, col = child.pos
self._tokens.append(
((line - 1, col - 1), len(child.value), "variable")
(
(line - 1, col - 1),
len(child.value),
"parameter",
[TokenModifier.declaration],
)
)
# Parameter mit Default-Wert ist meist eine List (z.B. {arg default})
@@ -85,7 +121,8 @@ class _Highlighter(Visitor):
(
(line - 1, col - 1),
len(name_node.value),
"variable",
"parameter",
[TokenModifier.declaration],
)
)
@@ -94,7 +131,7 @@ class _Highlighter(Visitor):
if hasattr(first_arg, "pos") and hasattr(first_arg, "value"):
line, col = first_arg.pos
self._tokens.append(
(((line - 1, col - 1), len(first_arg.value), "class"))
(((line - 1, col - 1), len(first_arg.value), "class", []))
)
def visit_var_sub(self, var_sub):
@@ -108,35 +145,15 @@ class _Highlighter(Visitor):
tokens = []
last_line = 0
last_col = 0
for (line, col), length, tok_type in sorted(self._tokens, key=lambda x: x[0]):
for (line, col), length, tok_type, tok_modifier in sorted(
self._tokens, key=lambda x: x[0]
):
line_delta = line - last_line
col_delta = col
if line == last_line:
col_delta -= last_col
tokens.append(Token(line_delta, col_delta, length, tok_type))
tokens.append(Token(line_delta, col_delta, length, tok_type, tok_modifier))
last_line, last_col = line, col
return tokens
# @server.feature(
# lsp.TEXT_DOCUMENT_SEMANTIC_TOKENS_FULL,
# lsp.SemanticTokensLegend(token_types=["keyword"], token_modifiers=[]),
# )
# def semantic_tokens(ls: TclspServer, params: lsp.SemanticTokensParams):
# logging.debug("Received %s: %s", lsp.TEXT_DOCUMENT_SEMANTIC_TOKENS_FULL, params)
# document = ls.workspace.get_text_document(params.text_document.uri)
# path = Path(document.path)
# root = ls.get_root(path)
# config = ls.get_config(path, root)
# plugins = [config.commands] if config.commands is not None else []
# parser = Parser(command_plugins=plugins)
# hl = _Highlighter(plugins)
# tree = parser.parse(document.source)
# tree.accept(hl, recurse=True)
# return lsp.SemanticTokens(data=hl.tokens())