add some features
This commit is contained in:
@@ -179,11 +179,11 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
|
||||
context.subscriptions.push(formatDefProvider)
|
||||
|
||||
// const tclOutlineProvider = vscode.languages.registerDocumentSymbolProvider(
|
||||
// { scheme: "file", language: "tcl" },
|
||||
// { provideDocumentSymbols: tclDocumentSymbolProvider }
|
||||
// )
|
||||
// context.subscriptions.push(tclOutlineProvider)
|
||||
const tclOutlineProvider = vscode.languages.registerDocumentSymbolProvider(
|
||||
{ scheme: "file", language: "tcl" },
|
||||
{ provideDocumentSymbols: tclDocumentSymbolProvider }
|
||||
)
|
||||
context.subscriptions.push(tclOutlineProvider)
|
||||
|
||||
// Diagnostics collection
|
||||
const diagnosticCollectionCdl = vscode.languages.createDiagnosticCollection("cdl")
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import re
|
||||
|
||||
|
||||
def format_tcl(src: str, indent_str=" ") -> str:
|
||||
"""
|
||||
Simple Tcl formatter with special handling for:
|
||||
1) Single-line 'if {cond} {action}' blocks remain on one line.
|
||||
2) Combined closing-and-opening lines like '} else {' dedent then re-indent.
|
||||
3) Lines like '} Tag' stay on the same line: '} Tag'.
|
||||
4) Standard multi-line blocks for 'if', 'elseif', 'else', '{', '}'.
|
||||
"""
|
||||
level = 0
|
||||
out_lines = []
|
||||
|
||||
for raw_line in src.splitlines():
|
||||
stripped = raw_line.strip()
|
||||
|
||||
# Comments are ignored
|
||||
if stripped.startswith("#"):
|
||||
out_lines.append(indent_str * level + stripped)
|
||||
continue
|
||||
|
||||
# Single-line 'if {cond} {action}' → no indent change
|
||||
if re.match(r"^(if|elseif)\s*\{[^}]+\}\s*\{[^}]+\}$", stripped):
|
||||
out_lines.append(indent_str * level + stripped)
|
||||
continue
|
||||
|
||||
# Combined '} else {' → dedent, print, then indent
|
||||
if re.match(r"^\}\s*(elseif|else)\b.*\{$", stripped):
|
||||
level = max(level - 1, 0)
|
||||
out_lines.append(indent_str * level + stripped)
|
||||
level += 1
|
||||
continue
|
||||
|
||||
# SPECIAL: closing brace plus tag on same line: '} Tag'
|
||||
m = re.match(r"^\}\s+(.+)", stripped)
|
||||
if m and not stripped.startswith("#"):
|
||||
# close one block
|
||||
level = max(level - 1, 0)
|
||||
# stay on one line: "} Tag"
|
||||
out_lines.append(f"{indent_str * level}}} {m.group(1)}")
|
||||
continue
|
||||
|
||||
# Pure '}' → dedent then print
|
||||
if stripped == "}":
|
||||
level = max(level - 1, 0)
|
||||
out_lines.append(f"{indent_str * level}{stripped}")
|
||||
continue
|
||||
|
||||
# 'elseif' or 'else' alone → align with matching 'if'
|
||||
if re.match(r"^(elseif|else)\b(?!.*\{)", stripped):
|
||||
level = max(level - 1, 0)
|
||||
out_lines.append(f"{indent_str * level}{stripped}")
|
||||
continue
|
||||
|
||||
# Default: print at current indent
|
||||
out_lines.append(f"{indent_str * level}{stripped}")
|
||||
|
||||
# Open a new block on lines ending with '{'
|
||||
if re.match(r"^(if|elseif)\b.*\{$", stripped) or stripped.endswith("{"):
|
||||
level += 1
|
||||
|
||||
return "\n".join(out_lines)
|
||||
+15
-28
@@ -4,16 +4,12 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
import sysconfig
|
||||
import traceback
|
||||
from typing import Any, List, Optional, Sequence, Tuple
|
||||
import re
|
||||
from typing import Any, List, Optional, Tuple
|
||||
import operator
|
||||
from functools import reduce
|
||||
|
||||
@@ -41,22 +37,18 @@ update_sys_path(
|
||||
# **********************************************************
|
||||
# pylint: disable=wrong-import-position,import-error
|
||||
import lsp_jsonrpc as jsonrpc
|
||||
import lsp_utils as utils
|
||||
import lsprotocol.types as lsp
|
||||
from pygls import server, uris, workspace
|
||||
from pygls.workspace.text_document import TextDocument
|
||||
from common.load_data import standard_items
|
||||
from tclint.parser import Parser
|
||||
from tclint.lexer import TclSyntaxError
|
||||
from tclint.format import Formatter, FormatterOpts
|
||||
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.inlay_hint import InlayHintGenerator
|
||||
from tools.symbols import OutlineVisitor
|
||||
|
||||
DIAGNOSTIC_SOURCE = "nx-post-support"
|
||||
|
||||
@@ -64,7 +56,7 @@ DIAGNOSTIC_SOURCE = "nx-post-support"
|
||||
class TclLanguageServer(server.LanguageServer):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.parser = Parser()
|
||||
self.parser = parser.CustomParser()
|
||||
for command in commands:
|
||||
self.parser._commands.update(command)
|
||||
self.diagnostics = {}
|
||||
@@ -197,14 +189,15 @@ def did_open(params: lsp.DidOpenTextDocumentParams) -> None:
|
||||
"""LSP handler for textDocument/didOpen request."""
|
||||
document = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
|
||||
LSP_SERVER.compute_diagnostics(document)
|
||||
completion.reset()
|
||||
tree = LSP_SERVER.parser.parse(document.source)
|
||||
tree.accept(completion, recurse=True)
|
||||
|
||||
|
||||
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DID_SAVE)
|
||||
def did_save(params: lsp.DidSaveTextDocumentParams) -> None:
|
||||
"""LSP handler for textDocument/didSave request."""
|
||||
document = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
|
||||
tree = LSP_SERVER.parser.parse(document.source)
|
||||
log_to_output(tree.pretty(2))
|
||||
_ = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
|
||||
|
||||
|
||||
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DID_CLOSE)
|
||||
@@ -220,7 +213,6 @@ def did_change(params: lsp.DidChangeTextDocumentParams) -> None:
|
||||
completion.reset()
|
||||
tree = LSP_SERVER.parser.parse(document.source)
|
||||
tree.accept(completion, recurse=True)
|
||||
log_to_output(tree.pretty(2))
|
||||
|
||||
|
||||
@LSP_SERVER.feature(
|
||||
@@ -250,7 +242,7 @@ def document_diagnostic(params: lsp.DocumentDiagnosticParams):
|
||||
|
||||
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_COMPLETION)
|
||||
def on_completion(params: lsp.CompletionParams) -> list[lsp.CompletionItem]:
|
||||
document = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
|
||||
_ = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
|
||||
|
||||
items = (
|
||||
standard_items.tcl_keyword_list
|
||||
@@ -261,15 +253,10 @@ def on_completion(params: lsp.CompletionParams) -> list[lsp.CompletionItem]:
|
||||
return lsp.CompletionList(is_incomplete=False, items=items)
|
||||
|
||||
|
||||
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DOCUMENT_SYMBOL)
|
||||
def document_symbols(params: lsp.DocumentSymbolParams):
|
||||
doc = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
|
||||
tree = LSP_SERVER.parser.parse(doc.source)
|
||||
|
||||
visitor = OutlineVisitor(LSP_SERVER.parser)
|
||||
tree.accept(visitor, recurse=True)
|
||||
|
||||
return visitor.stack[0]
|
||||
# @LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DOCUMENT_SYMBOL)
|
||||
# def document_symbols(params: lsp.DocumentSymbolParams):
|
||||
# doc = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
|
||||
# return []
|
||||
|
||||
|
||||
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_INLAY_HINT)
|
||||
@@ -277,7 +264,7 @@ def inlay_hints(params: lsp.InlayHintParams):
|
||||
document = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
|
||||
tree = LSP_SERVER.parser.parse(document.source)
|
||||
|
||||
# Inlay Hints sammeln
|
||||
# collect Inlay Hints
|
||||
generator = InlayHintGenerator(completion.proc_signatures)
|
||||
tree.accept(generator, recurse=True)
|
||||
|
||||
@@ -405,12 +392,12 @@ def formatting(params: lsp.DocumentFormattingParams) -> list[lsp.TextEdit] | Non
|
||||
start = lsp.Position(line=0, character=0)
|
||||
last_line = source.rsplit("\n", 1)[-1]
|
||||
end = lsp.Position(line=source.count("\n"), character=len(last_line))
|
||||
|
||||
formatted = LSP_SERVER.format(doc, params.options)
|
||||
if GLOBAL_SETTINGS.get("formatter", True):
|
||||
source = LSP_SERVER.format(doc, params.options)
|
||||
return [
|
||||
lsp.TextEdit(
|
||||
range=lsp.Range(start=start, end=end),
|
||||
new_text=formatted,
|
||||
new_text=source,
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
+330
-117
@@ -1,134 +1,347 @@
|
||||
from tclint.parser import Parser, _strip_ws
|
||||
from tclint.lexer import (
|
||||
STATE_BRACEDWORD,
|
||||
TOK_BACKSLASH_NEWLINE,
|
||||
TOK_EOF,
|
||||
TOK_LBRACE,
|
||||
TOK_RBRACE,
|
||||
TOK_WS,
|
||||
TOK_NEWLINE,
|
||||
TOK_ALPHA_CHARS,
|
||||
TOK_RPAREN,
|
||||
TOK_LPAREN,
|
||||
TOK_DOLLAR,
|
||||
TOK_QUOTE,
|
||||
TOK_LBRACKET,
|
||||
TOK_NUM_CHARS,
|
||||
TclSyntaxError,
|
||||
)
|
||||
from tclint.parser import Parser
|
||||
from tclint.commands import CommandArgError
|
||||
from tclint.syntax_tree import (
|
||||
BracedWord,
|
||||
BareWord,
|
||||
BinaryOp,
|
||||
TernaryOp,
|
||||
ParenExpression,
|
||||
UnaryOp,
|
||||
BracedExpression,
|
||||
List,
|
||||
QuotedWord,
|
||||
Expression,
|
||||
)
|
||||
|
||||
import ply.lex as lex
|
||||
from typing import Tuple
|
||||
|
||||
TOK_BACKSLASH_NEWLINE = "BACKSLASH_NEWLINE"
|
||||
TOK_BACKSLASH_SUB = "BACKSLASH_SUB"
|
||||
TOK_NEWLINE = "NEWLINE"
|
||||
TOK_SEMI = "SEMI"
|
||||
TOK_WS = "WS"
|
||||
TOK_QUOTE = "QUOTE"
|
||||
TOK_ARG_EXPANSION = "ARG_EXPANSION"
|
||||
TOK_LBRACE = "LBRACE"
|
||||
TOK_RBRACE = "RBRACE"
|
||||
TOK_STAR = "STAR"
|
||||
TOK_LBRACKET = "LBRACKET"
|
||||
TOK_RBRACKET = "RBRACKET"
|
||||
TOK_DOLLAR = "DOLLAR"
|
||||
TOK_LPAREN = "LPAREN"
|
||||
TOK_RPAREN = "RPAREN"
|
||||
TOK_HASH = "HASH"
|
||||
TOK_ALPHA_CHARS = "ALPHA_CHARS"
|
||||
TOK_NUM_CHARS = "NUM_CHARS"
|
||||
TOK_NAMESPACE_SEP = "NAMESPACE_SEP"
|
||||
TOK_CHAR = "CHAR"
|
||||
TOK_CONTENTS = "CONTENTS"
|
||||
TOK_EOF = None
|
||||
|
||||
STATE_BRACEDWORD = "bracedword"
|
||||
|
||||
|
||||
class TclSyntaxError(Exception):
|
||||
def __init__(self, message, start: Tuple[int, int], end: Tuple[int, int]):
|
||||
super().__init__(message)
|
||||
self.start = start
|
||||
self.end = end
|
||||
|
||||
|
||||
class _LexTable:
|
||||
tokens = (
|
||||
TOK_BACKSLASH_NEWLINE,
|
||||
TOK_BACKSLASH_SUB,
|
||||
TOK_NEWLINE,
|
||||
TOK_SEMI,
|
||||
TOK_WS,
|
||||
TOK_QUOTE,
|
||||
TOK_ARG_EXPANSION,
|
||||
TOK_LBRACE,
|
||||
TOK_RBRACE,
|
||||
TOK_STAR,
|
||||
TOK_LBRACKET,
|
||||
TOK_RBRACKET,
|
||||
TOK_DOLLAR,
|
||||
TOK_LPAREN,
|
||||
TOK_RPAREN,
|
||||
TOK_HASH,
|
||||
TOK_ALPHA_CHARS,
|
||||
TOK_NUM_CHARS,
|
||||
TOK_NAMESPACE_SEP,
|
||||
TOK_CHAR,
|
||||
TOK_CONTENTS,
|
||||
)
|
||||
|
||||
# This defines a conditional lexing state for parsing braced words. This is a
|
||||
# performance optimization; since there are few special characters in this context,
|
||||
# we can use a smaller set of tokens to parse them faster. This has a large impact
|
||||
# since most Tcl programs have a large number of braced words. Any token with
|
||||
# `bracedword` in its name is included in this state. Tokens that are included in
|
||||
# this state and the default state also include `INITIAL` in their name.
|
||||
states = ((STATE_BRACEDWORD, "exclusive"),)
|
||||
|
||||
def _tok(self, t):
|
||||
pos = (t.lexer.lineno, t.lexer.colno)
|
||||
t.lexer.lineno += t.value.count("\n")
|
||||
index = t.value.rfind("\n")
|
||||
if index == -1:
|
||||
t.lexer.colno += len(t.value)
|
||||
else:
|
||||
remaining = t.value[index + 1 :]
|
||||
t.lexer.colno = len(remaining) + 1
|
||||
|
||||
t.value = (t.value, pos)
|
||||
return t
|
||||
|
||||
# Priority important
|
||||
def t_bracedword_INITIAL_BACKSLASH_NEWLINE(self, t):
|
||||
r"\\\r?\n"
|
||||
return self._tok(t)
|
||||
|
||||
# Priority important
|
||||
def t_bracedword_INITIAL_BACKSLASH_SUB(self, t):
|
||||
r"\\."
|
||||
return self._tok(t)
|
||||
|
||||
def t_NEWLINE(self, t):
|
||||
r"\n"
|
||||
return self._tok(t)
|
||||
|
||||
def t_SEMI(self, t):
|
||||
r";"
|
||||
return self._tok(t)
|
||||
|
||||
# TODO: should use \s?
|
||||
def t_WS(self, t):
|
||||
r"[\t\v\f\r ]+"
|
||||
return self._tok(t)
|
||||
|
||||
def t_QUOTE(self, t):
|
||||
r'"'
|
||||
return self._tok(t)
|
||||
|
||||
# Must be higher priority than LBRACE
|
||||
def t_ARG_EXPANSION(self, t):
|
||||
r"\{\*\}"
|
||||
return self._tok(t)
|
||||
|
||||
def t_bracedword_INITIAL_LBRACE(self, t):
|
||||
r"\{"
|
||||
return self._tok(t)
|
||||
|
||||
def t_bracedword_INITIAL_RBRACE(self, t):
|
||||
r"\}"
|
||||
return self._tok(t)
|
||||
|
||||
def t_STAR(self, t):
|
||||
r"\*"
|
||||
return self._tok(t)
|
||||
|
||||
def t_LBRACKET(self, t):
|
||||
r"\["
|
||||
return self._tok(t)
|
||||
|
||||
def t_RBRACKET(self, t):
|
||||
r"\]"
|
||||
return self._tok(t)
|
||||
|
||||
def t_DOLLAR(self, t):
|
||||
r"\$"
|
||||
return self._tok(t)
|
||||
|
||||
def t_LPAREN(self, t):
|
||||
r"\("
|
||||
return self._tok(t)
|
||||
|
||||
def t_RPAREN(self, t):
|
||||
r"\)"
|
||||
return self._tok(t)
|
||||
|
||||
def t_HASH(self, t):
|
||||
r"\#"
|
||||
return self._tok(t)
|
||||
|
||||
# Valid non-numeric chars in variable names
|
||||
def t_ALPHA_CHARS(self, t):
|
||||
r"[A-Za-z_]+"
|
||||
return self._tok(t)
|
||||
|
||||
# Valid numeric chars in variable names
|
||||
# This is split up from the above to facilitate expression parsing, since
|
||||
# e.g. 1eq1 can't be a single token.
|
||||
def t_NUM_CHARS(self, t):
|
||||
r"[0-9]+"
|
||||
return self._tok(t)
|
||||
|
||||
def t_NAMESPACE_SEP(self, t):
|
||||
r"::+"
|
||||
return self._tok(t)
|
||||
|
||||
def t_bracedword_CONTENTS(self, t):
|
||||
r"[^{}\\]+"
|
||||
return self._tok(t)
|
||||
|
||||
# Catch-all. TODO: inefficient, should probably munch multiple chars
|
||||
def t_CHAR(self, t):
|
||||
r"."
|
||||
return self._tok(t)
|
||||
|
||||
# Error handling rule
|
||||
# TODO: do we need this? since we have a catch-all...
|
||||
# there is a warning
|
||||
def t_bracedword_INITIAL_error(self, t):
|
||||
print("Illegal character '%s'" % t.value[0])
|
||||
t.lexer.skip(1)
|
||||
|
||||
def __init__(self):
|
||||
self.lexer = lex.lex(object=self)
|
||||
self.lexer.lineno = 1
|
||||
self.lexer.colno = 1
|
||||
|
||||
def new_lexer(self, pos=None):
|
||||
lexer = self.lexer.clone()
|
||||
lexer.lineno = 1
|
||||
lexer.colno = 1
|
||||
|
||||
if pos is not None:
|
||||
line, col = pos
|
||||
lexer.lineno = line
|
||||
lexer.colno = col
|
||||
|
||||
return lexer
|
||||
|
||||
|
||||
# Calling `lex.lex()` performs an expensive reflection process to generate the lexer.
|
||||
# This singleton class holds a preinitialized lexer that can then be cloned to create
|
||||
# individual instances.
|
||||
LexTable = _LexTable()
|
||||
|
||||
|
||||
class Lexer:
|
||||
def __init__(self, pos=None):
|
||||
self.lexer = LexTable.new_lexer(pos)
|
||||
self.current = None
|
||||
|
||||
def input(self, text):
|
||||
self.lexer.input(text)
|
||||
self.current = self.lexer.token()
|
||||
|
||||
def type(self):
|
||||
if self.current is None:
|
||||
return TOK_EOF
|
||||
return self.current.type
|
||||
|
||||
def value(self):
|
||||
if self.current is None:
|
||||
return None
|
||||
return self.current.value[0]
|
||||
|
||||
def pos(self):
|
||||
if self.current is None:
|
||||
return (self.lexer.lineno, self.lexer.colno)
|
||||
return self.current.value[1]
|
||||
|
||||
def next(self):
|
||||
self.current = self.lexer.token()
|
||||
|
||||
def expect(self, *tokens, message, pos):
|
||||
if self.type() not in tokens:
|
||||
self.next() # munch another token to update position
|
||||
raise TclSyntaxError(message, pos, self.pos())
|
||||
|
||||
self.next()
|
||||
|
||||
def assert_(self, *tokens):
|
||||
assert self.current.type in tokens
|
||||
self.next()
|
||||
|
||||
|
||||
class CustomParser(Parser):
|
||||
@_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,
|
||||
TOK_BACKSLASH_NEWLINE,
|
||||
} and ts.value() not in {":", ","}:
|
||||
if ts.value() == "?":
|
||||
# weird hack to record operator
|
||||
start = ts.pos()
|
||||
ts.next()
|
||||
q = BareWord("?", pos=start, end_pos=ts.pos())
|
||||
|
||||
op2 = self._parse_expression(ts)
|
||||
if ts.value() != ":":
|
||||
start = ts.pos()
|
||||
ts.next()
|
||||
end = ts.pos()
|
||||
raise TclSyntaxError(
|
||||
"expected ':' to continue ternary expression", start, end
|
||||
def parse(self, script, pos=None):
|
||||
lexer = Lexer(pos=pos)
|
||||
lexer.input(script)
|
||||
tree = self._parse_script(lexer, in_command_sub=False)
|
||||
assert lexer.type() == TOK_EOF, (
|
||||
"Didn't reach EOF parsing script, please file a bug report."
|
||||
)
|
||||
|
||||
# weird hack again
|
||||
start = ts.pos()
|
||||
ts.next()
|
||||
colon = BareWord(":", pos=start, end_pos=ts.pos())
|
||||
return tree
|
||||
|
||||
op3 = self._parse_expression(ts)
|
||||
expr = TernaryOp(
|
||||
op1, q, op2, colon, op3, pos=op1.pos, end_pos=op3.end_pos
|
||||
def parse_list(self, node):
|
||||
"""Parse contents of node as Tcl list. This is a distinct entry point
|
||||
that doesn't get used when generating the main syntax tree, but is used
|
||||
in command-specific argument parsing.
|
||||
"""
|
||||
if isinstance(node, List):
|
||||
return node
|
||||
|
||||
if node.contents is None:
|
||||
raise CommandArgError(
|
||||
"expected braced word or word without substitutions in argument"
|
||||
" interpreted as list"
|
||||
)
|
||||
|
||||
ts = Lexer(pos=node.contents_pos)
|
||||
ts.input(node.contents)
|
||||
|
||||
DELIMITERS = {TOK_WS, TOK_BACKSLASH_NEWLINE, TOK_NEWLINE}
|
||||
|
||||
list_node = List(pos=node.pos, end_pos=node.end_pos)
|
||||
while ts.type() is not TOK_EOF:
|
||||
while ts.type() in DELIMITERS:
|
||||
ts.next()
|
||||
|
||||
if ts.type() is TOK_EOF:
|
||||
break
|
||||
|
||||
if ts.type() == TOK_LBRACE:
|
||||
# we can reuse parse_braced_word, since it doesn't use
|
||||
# substitutions in any case
|
||||
list_node.add(self.parse_braced_word(ts))
|
||||
elif ts.type() == TOK_QUOTE:
|
||||
quote_word_pos = ts.pos()
|
||||
|
||||
ts.assert_(TOK_QUOTE)
|
||||
|
||||
bare_word_pos = ts.pos()
|
||||
contents = ""
|
||||
while ts.type() not in {TOK_QUOTE, TOK_EOF}:
|
||||
contents += ts.value()
|
||||
ts.next()
|
||||
word = BareWord(contents, pos=bare_word_pos, end_pos=ts.pos())
|
||||
|
||||
ts.expect(
|
||||
TOK_QUOTE,
|
||||
message="reached EOF without finding match for quote",
|
||||
pos=quote_word_pos,
|
||||
)
|
||||
|
||||
list_node.add(QuotedWord(word, pos=quote_word_pos, end_pos=ts.pos()))
|
||||
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() 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
|
||||
|
||||
def _parse_operator(self, ts):
|
||||
pos = ts.pos()
|
||||
contents = ""
|
||||
while ts.type() not in {*DELIMITERS, TOK_EOF}:
|
||||
contents += ts.value()
|
||||
ts.next()
|
||||
list_node.add(BareWord(contents, pos=pos, end_pos=ts.pos()))
|
||||
|
||||
# hacky logic to handle parsing legal operators
|
||||
return list_node
|
||||
|
||||
# Skip any backslash-newlines before the operator
|
||||
while ts.type() in {TOK_WS, TOK_BACKSLASH_NEWLINE, TOK_NEWLINE}:
|
||||
ts.next()
|
||||
|
||||
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()
|
||||
def parse_expression(self, node):
|
||||
if node.contents is None:
|
||||
raise CommandArgError(
|
||||
"expected braced word or word without substitutions in argument"
|
||||
" interpreted as expr"
|
||||
)
|
||||
|
||||
return BareWord(operator, pos=pos, end_pos=ts.pos())
|
||||
ts = Lexer(pos=node.contents_pos)
|
||||
ts.input(node.contents)
|
||||
|
||||
contents = self._parse_expression(ts)
|
||||
ts.expect(
|
||||
TOK_EOF,
|
||||
message=f"expected end of expression, got {ts.value()}",
|
||||
pos=ts.pos(),
|
||||
)
|
||||
if isinstance(node, BracedWord):
|
||||
return BracedExpression(contents, pos=node.pos, end_pos=node.end_pos)
|
||||
|
||||
return Expression(contents, pos=node.pos, end_pos=node.end_pos)
|
||||
|
||||
@@ -13,6 +13,7 @@ class TokenModifier(enum.IntFlag):
|
||||
defaultLibrary = enum.auto()
|
||||
definition = enum.auto()
|
||||
declaration = enum.auto()
|
||||
builtin = enum.auto()
|
||||
|
||||
|
||||
@attrs.define
|
||||
@@ -63,6 +64,20 @@ class _Highlighter(Visitor):
|
||||
def visit_command(self, command: Command):
|
||||
routine = command.routine
|
||||
|
||||
if routine.contents == "puts":
|
||||
line, col = routine.contents_pos
|
||||
self._tokens.append(
|
||||
(
|
||||
(
|
||||
(line - 1, col - 1),
|
||||
len(routine.contents),
|
||||
"function",
|
||||
[TokenModifier.builtin],
|
||||
)
|
||||
)
|
||||
)
|
||||
pass
|
||||
|
||||
if routine.contents == "set" and command.args:
|
||||
first_arg = command.args[0]
|
||||
if hasattr(first_arg, "pos") and hasattr(first_arg, "value"):
|
||||
@@ -135,7 +150,6 @@ class _Highlighter(Visitor):
|
||||
)
|
||||
|
||||
def visit_var_sub(self, var_sub):
|
||||
self.log_to_output(str(var_sub))
|
||||
pass
|
||||
|
||||
def tokens(self) -> list[Token]:
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
from tclint.syntax_tree import Visitor, BareWord, BracedWord, Script, Command
|
||||
from tclint.parser import Parser
|
||||
import lsprotocol.types as lsp
|
||||
|
||||
|
||||
class OutlineVisitor(Visitor):
|
||||
def __init__(self, parser: Parser):
|
||||
self.parser = parser
|
||||
self.stack = [[]] # Root symbol list
|
||||
|
||||
def _range(self, node) -> lsp.Range:
|
||||
line = node.line - 1
|
||||
col = node.col - 1
|
||||
if node.end_pos:
|
||||
end_line = node.end_pos[0] - 1
|
||||
end_col = node.end_pos[1] - 1
|
||||
else:
|
||||
end_line = line
|
||||
end_col = col + 1
|
||||
|
||||
return lsp.Range(
|
||||
start=lsp.Position(line=line, character=col),
|
||||
end=lsp.Position(line=end_line, character=end_col),
|
||||
)
|
||||
|
||||
def _add(self, name: str, kind: lsp.SymbolKind, node, children=None):
|
||||
symbol = lsp.DocumentSymbol(
|
||||
name=name,
|
||||
kind=kind,
|
||||
range=self._range(node),
|
||||
selection_range=self._range(node),
|
||||
children=children or [],
|
||||
)
|
||||
self.stack[-1].append(symbol)
|
||||
return symbol
|
||||
|
||||
def visit_script(self, script):
|
||||
for child in script.children:
|
||||
child.accept(self, recurse=False)
|
||||
|
||||
def visit_command(self, command: Command):
|
||||
if not isinstance(command.routine, BareWord):
|
||||
return
|
||||
name = command.routine.contents
|
||||
|
||||
# --- NAMESPACE EVAL ---
|
||||
if name == "namespace" and len(command.args) >= 3:
|
||||
subcmd = command.args[0]
|
||||
if isinstance(subcmd, BareWord) and subcmd.contents == "eval":
|
||||
ns_arg = command.args[1]
|
||||
ns_name = (
|
||||
ns_arg.contents if isinstance(ns_arg, BareWord) else "<namespace>"
|
||||
)
|
||||
ns_body = command.args[2]
|
||||
|
||||
ns_symbol = self._add(
|
||||
ns_name, lsp.SymbolKind.Namespace, command, children=[]
|
||||
)
|
||||
self.stack.append(ns_symbol.children)
|
||||
|
||||
if isinstance(ns_body, BracedWord):
|
||||
try:
|
||||
subtree = self.parser.parse_script(ns_body)
|
||||
subtree.accept(self, recurse=False)
|
||||
except Exception as e:
|
||||
print(f"Failed parsing namespace body: {e}")
|
||||
|
||||
self.stack.pop()
|
||||
|
||||
# --- PROC ---
|
||||
elif name == "proc" and len(command.args) >= 1:
|
||||
proc_arg = command.args[0]
|
||||
proc_name = (
|
||||
proc_arg.contents if isinstance(proc_arg, BareWord) else "<proc>"
|
||||
)
|
||||
self._add(proc_name, lsp.SymbolKind.Function, command)
|
||||
|
||||
# --- SET ---
|
||||
elif name == "set" and len(command.args) >= 1:
|
||||
var_arg = command.args[0]
|
||||
var_name = var_arg.contents if isinstance(var_arg, BareWord) else "<var>"
|
||||
self._add(var_name, lsp.SymbolKind.Variable, command)
|
||||
|
||||
+3
-3
@@ -28,8 +28,8 @@ proc SERVICE_spacer_output {type {length 20} {line_num 0} {output 1}} {
|
||||
LIB_GE_message [string repeat $type $length] "output_$output" $line_num
|
||||
}
|
||||
|
||||
SERVICE_spacer_output "*" 50 0 1
|
||||
SERVICE_remove_file $filename
|
||||
|
||||
SERVICE_spacer_output "*" 2 0 0
|
||||
|
||||
#_________________________________________________________________________________________________
|
||||
# <Documentation>
|
||||
@@ -93,7 +93,7 @@ proc SERVICE_output_handling {handler} {
|
||||
#_________________________________________________________________________________________________
|
||||
proc SERVICE_get_tool_data {} {
|
||||
global mom_tool_data
|
||||
global mom_operation_info
|
||||
global mom_operation_info
|
||||
|
||||
set mom_tool_data(toollist) ""
|
||||
set operations $::mom_operation_name_list
|
||||
|
||||
Reference in New Issue
Block a user