update lsp_server
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
@@ -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())
|
||||
|
||||
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user