add some features
This commit is contained in:
@@ -179,11 +179,11 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
|
|
||||||
context.subscriptions.push(formatDefProvider)
|
context.subscriptions.push(formatDefProvider)
|
||||||
|
|
||||||
// const tclOutlineProvider = vscode.languages.registerDocumentSymbolProvider(
|
const tclOutlineProvider = vscode.languages.registerDocumentSymbolProvider(
|
||||||
// { scheme: "file", language: "tcl" },
|
{ scheme: "file", language: "tcl" },
|
||||||
// { provideDocumentSymbols: tclDocumentSymbolProvider }
|
{ provideDocumentSymbols: tclDocumentSymbolProvider }
|
||||||
// )
|
)
|
||||||
// context.subscriptions.push(tclOutlineProvider)
|
context.subscriptions.push(tclOutlineProvider)
|
||||||
|
|
||||||
// Diagnostics collection
|
// Diagnostics collection
|
||||||
const diagnosticCollectionCdl = vscode.languages.createDiagnosticCollection("cdl")
|
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
|
from __future__ import annotations
|
||||||
|
|
||||||
import copy
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
import sysconfig
|
from typing import Any, List, Optional, Tuple
|
||||||
import traceback
|
|
||||||
from typing import Any, List, Optional, Sequence, Tuple
|
|
||||||
import re
|
|
||||||
import operator
|
import operator
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
|
|
||||||
@@ -41,22 +37,18 @@ update_sys_path(
|
|||||||
# **********************************************************
|
# **********************************************************
|
||||||
# pylint: disable=wrong-import-position,import-error
|
# pylint: disable=wrong-import-position,import-error
|
||||||
import lsp_jsonrpc as jsonrpc
|
import lsp_jsonrpc as jsonrpc
|
||||||
import lsp_utils as utils
|
|
||||||
import lsprotocol.types as lsp
|
import lsprotocol.types as lsp
|
||||||
from pygls import server, uris, workspace
|
from pygls import server, uris, workspace
|
||||||
from pygls.workspace.text_document import TextDocument
|
from pygls.workspace.text_document import TextDocument
|
||||||
from common.load_data import standard_items
|
from common.load_data import standard_items
|
||||||
from tclint.parser import Parser
|
|
||||||
from tclint.lexer import TclSyntaxError
|
from tclint.lexer import TclSyntaxError
|
||||||
from tclint.format import Formatter, FormatterOpts
|
from tclint.format import Formatter, FormatterOpts
|
||||||
from tclint.violations import Violation
|
from tclint.violations import Violation
|
||||||
|
|
||||||
from plugins.poco_plugin import commands
|
from plugins.poco_plugin import commands
|
||||||
from tools import checks, parser
|
from tools import checks, parser
|
||||||
from tools.semantic_tokens import _Highlighter, TOKEN_TYPES, TokenModifier
|
from tools.semantic_tokens import _Highlighter, TOKEN_TYPES, TokenModifier
|
||||||
from tools.completion_items import completion
|
from tools.completion_items import completion
|
||||||
from tools.inlay_hint import InlayHintGenerator
|
from tools.inlay_hint import InlayHintGenerator
|
||||||
from tools.symbols import OutlineVisitor
|
|
||||||
|
|
||||||
DIAGNOSTIC_SOURCE = "nx-post-support"
|
DIAGNOSTIC_SOURCE = "nx-post-support"
|
||||||
|
|
||||||
@@ -64,7 +56,7 @@ DIAGNOSTIC_SOURCE = "nx-post-support"
|
|||||||
class TclLanguageServer(server.LanguageServer):
|
class TclLanguageServer(server.LanguageServer):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.parser = Parser()
|
self.parser = parser.CustomParser()
|
||||||
for command in commands:
|
for command in commands:
|
||||||
self.parser._commands.update(command)
|
self.parser._commands.update(command)
|
||||||
self.diagnostics = {}
|
self.diagnostics = {}
|
||||||
@@ -197,14 +189,15 @@ def did_open(params: lsp.DidOpenTextDocumentParams) -> None:
|
|||||||
"""LSP handler for textDocument/didOpen request."""
|
"""LSP handler for textDocument/didOpen request."""
|
||||||
document = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
|
document = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
|
||||||
LSP_SERVER.compute_diagnostics(document)
|
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)
|
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DID_SAVE)
|
||||||
def did_save(params: lsp.DidSaveTextDocumentParams) -> None:
|
def did_save(params: lsp.DidSaveTextDocumentParams) -> None:
|
||||||
"""LSP handler for textDocument/didSave request."""
|
"""LSP handler for textDocument/didSave request."""
|
||||||
document = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
|
_ = 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.feature(lsp.TEXT_DOCUMENT_DID_CLOSE)
|
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DID_CLOSE)
|
||||||
@@ -220,7 +213,6 @@ def did_change(params: lsp.DidChangeTextDocumentParams) -> None:
|
|||||||
completion.reset()
|
completion.reset()
|
||||||
tree = LSP_SERVER.parser.parse(document.source)
|
tree = LSP_SERVER.parser.parse(document.source)
|
||||||
tree.accept(completion, recurse=True)
|
tree.accept(completion, recurse=True)
|
||||||
log_to_output(tree.pretty(2))
|
|
||||||
|
|
||||||
|
|
||||||
@LSP_SERVER.feature(
|
@LSP_SERVER.feature(
|
||||||
@@ -250,7 +242,7 @@ def document_diagnostic(params: lsp.DocumentDiagnosticParams):
|
|||||||
|
|
||||||
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_COMPLETION)
|
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_COMPLETION)
|
||||||
def on_completion(params: lsp.CompletionParams) -> list[lsp.CompletionItem]:
|
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 = (
|
items = (
|
||||||
standard_items.tcl_keyword_list
|
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)
|
return lsp.CompletionList(is_incomplete=False, items=items)
|
||||||
|
|
||||||
|
|
||||||
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DOCUMENT_SYMBOL)
|
# @LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DOCUMENT_SYMBOL)
|
||||||
def document_symbols(params: lsp.DocumentSymbolParams):
|
# def document_symbols(params: lsp.DocumentSymbolParams):
|
||||||
doc = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
|
# doc = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
|
||||||
tree = LSP_SERVER.parser.parse(doc.source)
|
# return []
|
||||||
|
|
||||||
visitor = OutlineVisitor(LSP_SERVER.parser)
|
|
||||||
tree.accept(visitor, recurse=True)
|
|
||||||
|
|
||||||
return visitor.stack[0]
|
|
||||||
|
|
||||||
|
|
||||||
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_INLAY_HINT)
|
@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)
|
document = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
|
||||||
tree = LSP_SERVER.parser.parse(document.source)
|
tree = LSP_SERVER.parser.parse(document.source)
|
||||||
|
|
||||||
# Inlay Hints sammeln
|
# collect Inlay Hints
|
||||||
generator = InlayHintGenerator(completion.proc_signatures)
|
generator = InlayHintGenerator(completion.proc_signatures)
|
||||||
tree.accept(generator, recurse=True)
|
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)
|
start = lsp.Position(line=0, character=0)
|
||||||
last_line = source.rsplit("\n", 1)[-1]
|
last_line = source.rsplit("\n", 1)[-1]
|
||||||
end = lsp.Position(line=source.count("\n"), character=len(last_line))
|
end = lsp.Position(line=source.count("\n"), character=len(last_line))
|
||||||
|
if GLOBAL_SETTINGS.get("formatter", True):
|
||||||
formatted = LSP_SERVER.format(doc, params.options)
|
source = LSP_SERVER.format(doc, params.options)
|
||||||
return [
|
return [
|
||||||
lsp.TextEdit(
|
lsp.TextEdit(
|
||||||
range=lsp.Range(start=start, end=end),
|
range=lsp.Range(start=start, end=end),
|
||||||
new_text=formatted,
|
new_text=source,
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
+336
-123
@@ -1,134 +1,347 @@
|
|||||||
from tclint.parser import Parser, _strip_ws
|
from tclint.parser import Parser
|
||||||
from tclint.lexer import (
|
from tclint.commands import CommandArgError
|
||||||
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.syntax_tree import (
|
from tclint.syntax_tree import (
|
||||||
BracedWord,
|
BracedWord,
|
||||||
BareWord,
|
BareWord,
|
||||||
BinaryOp,
|
BracedExpression,
|
||||||
TernaryOp,
|
List,
|
||||||
ParenExpression,
|
QuotedWord,
|
||||||
UnaryOp,
|
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):
|
class CustomParser(Parser):
|
||||||
@_strip_ws
|
def parse(self, script, pos=None):
|
||||||
def _parse_expression(self, ts):
|
lexer = Lexer(pos=pos)
|
||||||
op1 = self._parse_operand(ts)
|
lexer.input(script)
|
||||||
expr = op1
|
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."
|
||||||
|
)
|
||||||
|
|
||||||
# Add TOK_BACKSLASH_NEWLINE to the tokens we need to skip
|
return tree
|
||||||
while ts.type() == TOK_BACKSLASH_NEWLINE:
|
|
||||||
ts.next()
|
|
||||||
|
|
||||||
# last condition is hack to break out of expression in case we're in ternary op
|
def parse_list(self, node):
|
||||||
if ts.type() not in {
|
"""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:
|
||||||
|
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()))
|
||||||
|
|
||||||
|
return list_node
|
||||||
|
|
||||||
|
def parse_expression(self, node):
|
||||||
|
if node.contents is None:
|
||||||
|
raise CommandArgError(
|
||||||
|
"expected braced word or word without substitutions in argument"
|
||||||
|
" interpreted as expr"
|
||||||
|
)
|
||||||
|
|
||||||
|
ts = Lexer(pos=node.contents_pos)
|
||||||
|
ts.input(node.contents)
|
||||||
|
|
||||||
|
contents = self._parse_expression(ts)
|
||||||
|
ts.expect(
|
||||||
TOK_EOF,
|
TOK_EOF,
|
||||||
TOK_RPAREN,
|
message=f"expected end of expression, got {ts.value()}",
|
||||||
TOK_BACKSLASH_NEWLINE,
|
pos=ts.pos(),
|
||||||
} and ts.value() not in {":", ","}:
|
)
|
||||||
if ts.value() == "?":
|
if isinstance(node, BracedWord):
|
||||||
# weird hack to record operator
|
return BracedExpression(contents, pos=node.pos, end_pos=node.end_pos)
|
||||||
start = ts.pos()
|
|
||||||
ts.next()
|
|
||||||
q = BareWord("?", pos=start, end_pos=ts.pos())
|
|
||||||
|
|
||||||
op2 = self._parse_expression(ts)
|
return Expression(contents, pos=node.pos, end_pos=node.end_pos)
|
||||||
if ts.value() != ":":
|
|
||||||
start = ts.pos()
|
|
||||||
ts.next()
|
|
||||||
end = ts.pos()
|
|
||||||
raise TclSyntaxError(
|
|
||||||
"expected ':' to continue ternary expression", start, end
|
|
||||||
)
|
|
||||||
|
|
||||||
# weird hack again
|
|
||||||
start = ts.pos()
|
|
||||||
ts.next()
|
|
||||||
colon = BareWord(":", pos=start, end_pos=ts.pos())
|
|
||||||
|
|
||||||
op3 = self._parse_expression(ts)
|
|
||||||
expr = TernaryOp(
|
|
||||||
op1, q, op2, colon, op3, pos=op1.pos, end_pos=op3.end_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()
|
|
||||||
|
|
||||||
# hacky logic to handle parsing legal operators
|
|
||||||
|
|
||||||
# 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()
|
|
||||||
)
|
|
||||||
|
|
||||||
return BareWord(operator, pos=pos, end_pos=ts.pos())
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ class TokenModifier(enum.IntFlag):
|
|||||||
defaultLibrary = enum.auto()
|
defaultLibrary = enum.auto()
|
||||||
definition = enum.auto()
|
definition = enum.auto()
|
||||||
declaration = enum.auto()
|
declaration = enum.auto()
|
||||||
|
builtin = enum.auto()
|
||||||
|
|
||||||
|
|
||||||
@attrs.define
|
@attrs.define
|
||||||
@@ -63,6 +64,20 @@ class _Highlighter(Visitor):
|
|||||||
def visit_command(self, command: Command):
|
def visit_command(self, command: Command):
|
||||||
routine = command.routine
|
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:
|
if routine.contents == "set" and command.args:
|
||||||
first_arg = command.args[0]
|
first_arg = command.args[0]
|
||||||
if hasattr(first_arg, "pos") and hasattr(first_arg, "value"):
|
if hasattr(first_arg, "pos") and hasattr(first_arg, "value"):
|
||||||
@@ -135,7 +150,6 @@ class _Highlighter(Visitor):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def visit_var_sub(self, var_sub):
|
def visit_var_sub(self, var_sub):
|
||||||
self.log_to_output(str(var_sub))
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def tokens(self) -> list[Token]:
|
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
|
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>
|
# <Documentation>
|
||||||
@@ -93,7 +93,7 @@ proc SERVICE_output_handling {handler} {
|
|||||||
#_________________________________________________________________________________________________
|
#_________________________________________________________________________________________________
|
||||||
proc SERVICE_get_tool_data {} {
|
proc SERVICE_get_tool_data {} {
|
||||||
global mom_tool_data
|
global mom_tool_data
|
||||||
global mom_operation_info
|
global mom_operation_info
|
||||||
|
|
||||||
set mom_tool_data(toollist) ""
|
set mom_tool_data(toollist) ""
|
||||||
set operations $::mom_operation_name_list
|
set operations $::mom_operation_name_list
|
||||||
|
|||||||
Reference in New Issue
Block a user