update formatter
This commit is contained in:
@@ -7,3 +7,5 @@ target
|
||||
__pycache__
|
||||
*.pyc
|
||||
.nox
|
||||
*.g4
|
||||
.antlr
|
||||
@@ -0,0 +1,58 @@
|
||||
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()
|
||||
|
||||
# 1) 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
|
||||
|
||||
# 2) 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
|
||||
|
||||
# 3) SPECIAL: closing brace plus tag on same line: '} Tag'
|
||||
m = re.match(r"^\}\s+(\w+)$", stripped)
|
||||
if m:
|
||||
# close one block
|
||||
level = max(level - 1, 0)
|
||||
# stay on one line: "} Tag"
|
||||
out_lines.append(f"{indent_str * level}}} {m.group(1)}")
|
||||
continue
|
||||
|
||||
# 4) Pure '}' → dedent then print
|
||||
if stripped == "}":
|
||||
level = max(level - 1, 0)
|
||||
out_lines.append(f"{indent_str * level}{stripped}")
|
||||
continue
|
||||
|
||||
# 5) '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
|
||||
|
||||
# 6) Default: print at current indent
|
||||
out_lines.append(f"{indent_str * level}{stripped}")
|
||||
|
||||
# 7) 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)
|
||||
@@ -0,0 +1,81 @@
|
||||
# tokens.py
|
||||
from typing import List
|
||||
import attrs
|
||||
import enum
|
||||
from lark import Tree, Token
|
||||
|
||||
# Legend must match the client
|
||||
SEMANTIC_TOKEN_TYPES = {
|
||||
"keyword": 0,
|
||||
"variable": 2,
|
||||
"string": 3,
|
||||
}
|
||||
SEMANTIC_TOKEN_MODIFIERS = {
|
||||
"declaration": 1 << 0,
|
||||
}
|
||||
|
||||
|
||||
class TokenModifier(enum.IntFlag):
|
||||
deprecated = enum.auto()
|
||||
readonly = enum.auto()
|
||||
defaultLibrary = enum.auto()
|
||||
definition = enum.auto()
|
||||
|
||||
|
||||
@attrs.define
|
||||
class TokenData:
|
||||
line: int
|
||||
offset: int
|
||||
text: str
|
||||
|
||||
tok_type: str = ""
|
||||
tok_modifiers: List[TokenModifier] = attrs.field(factory=list)
|
||||
|
||||
|
||||
TokenTypes = ["keyword", "variable", "function", "operator", "parameter", "type"]
|
||||
|
||||
|
||||
def collect_semantic_tokens(tree: Tree) -> list[int]:
|
||||
"""
|
||||
Walk the parse tree and return a flat LSP semanticTokens/full array:
|
||||
[line, char, length, tokenType, tokenModifiers, …]
|
||||
"""
|
||||
data = []
|
||||
for tok in tree.scan_values(lambda v: isinstance(v, Token)):
|
||||
# 'set' keyword
|
||||
if tok.type == "SET":
|
||||
data.append(
|
||||
[
|
||||
tok.line - 1,
|
||||
tok.column - 1,
|
||||
len(tok.value),
|
||||
SEMANTIC_TOKEN_TYPES["keyword"],
|
||||
0,
|
||||
]
|
||||
)
|
||||
# variable being declared
|
||||
elif tok.type == "NAME":
|
||||
data.append(
|
||||
[
|
||||
tok.line - 1,
|
||||
tok.column - 1,
|
||||
len(tok.value),
|
||||
SEMANTIC_TOKEN_TYPES["variable"],
|
||||
SEMANTIC_TOKEN_MODIFIERS["declaration"],
|
||||
]
|
||||
)
|
||||
# string literal
|
||||
elif tok.type == "STRING":
|
||||
data.append(
|
||||
[
|
||||
tok.line - 1,
|
||||
tok.column - 1,
|
||||
len(tok.value),
|
||||
SEMANTIC_TOKEN_TYPES["string"],
|
||||
0,
|
||||
]
|
||||
)
|
||||
|
||||
# sort by position and flatten
|
||||
data.sort(key=lambda x: (x[0], x[1]))
|
||||
return [p for token in data for p in token]
|
||||
+24
-328
@@ -13,7 +13,6 @@ import sys
|
||||
import sysconfig
|
||||
import traceback
|
||||
from typing import Any, Optional, Sequence
|
||||
from parser.parser import parse_tcl, format_tree
|
||||
|
||||
|
||||
# **********************************************************
|
||||
@@ -43,6 +42,8 @@ import lsp_utils as utils
|
||||
import lsprotocol.types as lsp
|
||||
from pygls import server, uris, workspace
|
||||
from common.load_data import standard_items
|
||||
from common.formatter import format_tcl
|
||||
|
||||
|
||||
WORKSPACE_SETTINGS = {}
|
||||
GLOBAL_SETTINGS = {}
|
||||
@@ -105,71 +106,11 @@ def on_completion(params: lsp.CompletionParams) -> list[lsp.CompletionItem]:
|
||||
return lsp.CompletionList(is_incomplete=False, items=items)
|
||||
|
||||
|
||||
# TODO: If your linter outputs in a known format like JSON, then parse
|
||||
# accordingly. But incase you need to parse the output using RegEx here
|
||||
# is a helper you can work with.
|
||||
# flake8 example:
|
||||
# If you use following format argument with flake8 you can use the regex below to parse it.
|
||||
# TOOL_ARGS += ["--format='%(row)d,%(col)d,%(code).1s,%(code)s:%(text)s'"]
|
||||
# DIAGNOSTIC_RE =
|
||||
# r"(?P<line>\d+),(?P<column>-?\d+),(?P<type>\w+),(?P<code>\w+\d+):(?P<message>[^\r\n]*)"
|
||||
DIAGNOSTIC_RE = re.compile(r"")
|
||||
|
||||
|
||||
def _parse_output_using_regex(content: str) -> list[lsp.Diagnostic]:
|
||||
lines: list[str] = content.splitlines()
|
||||
diagnostics: list[lsp.Diagnostic] = []
|
||||
|
||||
# TODO: Determine if your linter reports line numbers starting at 1 (True) or 0 (False).
|
||||
line_at_1 = True
|
||||
# TODO: Determine if your linter reports column numbers starting at 1 (True) or 0 (False).
|
||||
column_at_1 = True
|
||||
|
||||
line_offset = 1 if line_at_1 else 0
|
||||
col_offset = 1 if column_at_1 else 0
|
||||
for line in lines:
|
||||
if line.startswith("'") and line.endswith("'"):
|
||||
line = line[1:-1]
|
||||
match = DIAGNOSTIC_RE.match(line)
|
||||
if match:
|
||||
data = match.groupdict()
|
||||
position = lsp.Position(
|
||||
line=max([int(data["line"]) - line_offset, 0]),
|
||||
character=int(data["column"]) - col_offset,
|
||||
)
|
||||
diagnostic = lsp.Diagnostic(
|
||||
range=lsp.Range(
|
||||
start=position,
|
||||
end=position,
|
||||
),
|
||||
message=data.get("message"),
|
||||
severity=_get_severity(data["code"], data["type"]),
|
||||
code=data["code"],
|
||||
source=TOOL_MODULE,
|
||||
)
|
||||
diagnostics.append(diagnostic)
|
||||
|
||||
return diagnostics
|
||||
|
||||
|
||||
# TODO: if you want to handle setting specific severity for your linter
|
||||
# in a user configurable way, then look at look at how it is implemented
|
||||
# for `pylint` extension from our team.
|
||||
# Pylint: https://github.com/microsoft/vscode-pylint
|
||||
# Follow the flow of severity from the settings in package.json to the server.
|
||||
def _get_severity(*_codes: list[str]) -> lsp.DiagnosticSeverity:
|
||||
# TODO: All reported issues from linter are treated as warning.
|
||||
# change it as appropriate for your linter.
|
||||
return lsp.DiagnosticSeverity.Warning
|
||||
|
||||
|
||||
# **********************************************************
|
||||
# Linting features end here
|
||||
# **********************************************************
|
||||
|
||||
# TODO: If your tool is a formatter then update this section.
|
||||
# Delete "Formatting features" section if your tool is NOT a
|
||||
# formatter.
|
||||
|
||||
# **********************************************************
|
||||
# Formatting features start here
|
||||
# **********************************************************
|
||||
@@ -177,74 +118,31 @@ def _get_severity(*_codes: list[str]) -> lsp.DiagnosticSeverity:
|
||||
# Black: https://github.com/microsoft/vscode-black-formatter/blob/main/bundled/tool
|
||||
|
||||
|
||||
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_FORMATTING)
|
||||
def formatting(params: lsp.DocumentFormattingParams) -> list[lsp.TextEdit] | None:
|
||||
"""LSP handler for textDocument/formatting request."""
|
||||
text = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
|
||||
tree, _ = parse_tcl(text)
|
||||
if not tree:
|
||||
return []
|
||||
new_text = format_tree(tree)
|
||||
return [
|
||||
{
|
||||
"range": {
|
||||
"start": {"line": 0, "character": 0},
|
||||
"end": {"line": len(text.splitlines()), "character": 0},
|
||||
},
|
||||
"newText": new_text,
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def _formatting_helper(document: workspace.Document) -> list[lsp.TextEdit] | None:
|
||||
# TODO: For formatting on save support the formatter you use must support
|
||||
# formatting via stdin.
|
||||
# Read, and update_run_tool_on_document and _run_tool functions as needed
|
||||
# for your formatter.
|
||||
result = _run_tool_on_document(document, use_stdin=True)
|
||||
if result.stdout:
|
||||
new_source = _match_line_endings(document, result.stdout)
|
||||
return [
|
||||
lsp.TextEdit(
|
||||
range=lsp.Range(
|
||||
start=lsp.Position(line=0, character=0),
|
||||
end=lsp.Position(line=len(document.lines), character=0),
|
||||
),
|
||||
new_text=new_source,
|
||||
)
|
||||
]
|
||||
return None
|
||||
|
||||
|
||||
def _get_line_endings(lines: list[str]) -> str:
|
||||
"""Returns line endings used in the text."""
|
||||
try:
|
||||
if lines[0][-2:] == "\r\n":
|
||||
return "\r\n"
|
||||
return "\n"
|
||||
except Exception: # pylint: disable=broad-except
|
||||
return None
|
||||
|
||||
|
||||
def _match_line_endings(document: workspace.Document, text: str) -> str:
|
||||
"""Ensures that the edited text line endings matches the document line endings."""
|
||||
expected = _get_line_endings(document.source.splitlines(keepends=True))
|
||||
actual = _get_line_endings(text.splitlines(keepends=True))
|
||||
if actual == expected or actual is None or expected is None:
|
||||
return text
|
||||
return text.replace(actual, expected)
|
||||
|
||||
|
||||
# **********************************************************
|
||||
# Formatting features ends here
|
||||
# **********************************************************
|
||||
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_FORMATTING)
|
||||
def formatting(params: lsp.DocumentFormattingParams) -> list[lsp.TextEdit] | None:
|
||||
"""LSP handler for textDocument/formatting request."""
|
||||
doc = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
|
||||
text = doc.source
|
||||
|
||||
formatted = format_tcl(text)
|
||||
|
||||
last_line = len(text.splitlines())
|
||||
full_range = lsp.Range(
|
||||
start=lsp.Position(line=0, character=0),
|
||||
end=lsp.Position(line=last_line, character=0),
|
||||
)
|
||||
edit = lsp.TextEdit(range=full_range, new_text=formatted)
|
||||
return [edit]
|
||||
|
||||
|
||||
# **********************************************************
|
||||
# Required Language Server Initialization and Exit handlers.
|
||||
# **********************************************************
|
||||
@LSP_SERVER.feature(lsp.INITIALIZE)
|
||||
def initialize(params: lsp.InitializeParams) -> None:
|
||||
def initialize(params: lsp.InitializeParams) -> lsp.InitializeResult:
|
||||
"""LSP handler for initialize request."""
|
||||
log_to_output(f"CWD Server: {os.getcwd()}")
|
||||
|
||||
@@ -261,6 +159,11 @@ def initialize(params: lsp.InitializeParams) -> None:
|
||||
log_to_output(
|
||||
f"Global settings:\r\n{json.dumps(GLOBAL_SETTINGS, indent=4, ensure_ascii=False)}\r\n"
|
||||
)
|
||||
return lsp.InitializeResult(
|
||||
capabilities=lsp.ServerCapabilities(
|
||||
document_formatting_provider=True,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@LSP_SERVER.feature(lsp.EXIT)
|
||||
@@ -350,213 +253,6 @@ def _get_settings_by_document(document: workspace.Document | None):
|
||||
return WORKSPACE_SETTINGS[str(key)]
|
||||
|
||||
|
||||
# *****************************************************
|
||||
# Internal execution APIs.
|
||||
# *****************************************************
|
||||
def _run_tool_on_document(
|
||||
document: workspace.Document,
|
||||
use_stdin: bool = False,
|
||||
extra_args: Optional[Sequence[str]] = None,
|
||||
) -> utils.RunResult | None:
|
||||
"""Runs tool on the given document.
|
||||
|
||||
if use_stdin is true then contents of the document is passed to the
|
||||
tool via stdin.
|
||||
"""
|
||||
if extra_args is None:
|
||||
extra_args = []
|
||||
if str(document.uri).startswith("vscode-notebook-cell"):
|
||||
# TODO: Decide on if you want to skip notebook cells.
|
||||
# Skip notebook cells
|
||||
return None
|
||||
|
||||
if utils.is_stdlib_file(document.path):
|
||||
# TODO: Decide on if you want to skip standard library files.
|
||||
# Skip standard library python files.
|
||||
return None
|
||||
|
||||
# deep copy here to prevent accidentally updating global settings.
|
||||
settings = copy.deepcopy(_get_settings_by_document(document))
|
||||
|
||||
code_workspace = settings["workspaceFS"]
|
||||
cwd = settings["cwd"]
|
||||
|
||||
use_path = False
|
||||
use_rpc = False
|
||||
if settings["path"]:
|
||||
# 'path' setting takes priority over everything.
|
||||
use_path = True
|
||||
argv = settings["path"]
|
||||
elif settings["interpreter"] and not utils.is_current_interpreter(
|
||||
settings["interpreter"][0]
|
||||
):
|
||||
# If there is a different interpreter set use JSON-RPC to the subprocess
|
||||
# running under that interpreter.
|
||||
argv = [TOOL_MODULE]
|
||||
use_rpc = True
|
||||
else:
|
||||
# if the interpreter is same as the interpreter running this
|
||||
# process then run as module.
|
||||
argv = [TOOL_MODULE]
|
||||
|
||||
argv += TOOL_ARGS + settings["args"] + extra_args
|
||||
|
||||
if use_stdin:
|
||||
# TODO: update these to pass the appropriate arguments to provide document contents
|
||||
# to tool via stdin.
|
||||
# For example, for pylint args for stdin looks like this:
|
||||
# pylint --from-stdin <path>
|
||||
# Here `--from-stdin` path is used by pylint to make decisions on the file contents
|
||||
# that are being processed. Like, applying exclusion rules.
|
||||
# It should look like this when you pass it:
|
||||
# argv += ["--from-stdin", document.path]
|
||||
# Read up on how your tool handles contents via stdin. If stdin is not supported use
|
||||
# set use_stdin to False, or provide path, what ever is appropriate for your tool.
|
||||
argv += []
|
||||
else:
|
||||
argv += [document.path]
|
||||
|
||||
if use_path:
|
||||
# This mode is used when running executables.
|
||||
log_to_output(" ".join(argv))
|
||||
log_to_output(f"CWD Server: {cwd}")
|
||||
result = utils.run_path(
|
||||
argv=argv,
|
||||
use_stdin=use_stdin,
|
||||
cwd=cwd,
|
||||
source=document.source.replace("\r\n", "\n"),
|
||||
)
|
||||
if result.stderr:
|
||||
log_to_output(result.stderr)
|
||||
elif use_rpc:
|
||||
# This mode is used if the interpreter running this server is different from
|
||||
# the interpreter used for running this server.
|
||||
log_to_output(" ".join(settings["interpreter"] + ["-m"] + argv))
|
||||
log_to_output(f"CWD Linter: {cwd}")
|
||||
|
||||
result = jsonrpc.run_over_json_rpc(
|
||||
workspace=code_workspace,
|
||||
interpreter=settings["interpreter"],
|
||||
module=TOOL_MODULE,
|
||||
argv=argv,
|
||||
use_stdin=use_stdin,
|
||||
cwd=cwd,
|
||||
source=document.source,
|
||||
)
|
||||
if result.exception:
|
||||
log_error(result.exception)
|
||||
result = utils.RunResult(result.stdout, result.stderr)
|
||||
elif result.stderr:
|
||||
log_to_output(result.stderr)
|
||||
else:
|
||||
# In this mode the tool is run as a module in the same process as the language server.
|
||||
log_to_output(" ".join([sys.executable, "-m"] + argv))
|
||||
log_to_output(f"CWD Linter: {cwd}")
|
||||
# This is needed to preserve sys.path, in cases where the tool modifies
|
||||
# sys.path and that might not work for this scenario next time around.
|
||||
with utils.substitute_attr(sys, "path", sys.path[:]):
|
||||
try:
|
||||
# TODO: `utils.run_module` is equivalent to running `python -m <pytool-module>`.
|
||||
# If your tool supports a programmatic API then replace the function below
|
||||
# with code for your tool. You can also use `utils.run_api` helper, which
|
||||
# handles changing working directories, managing io streams, etc.
|
||||
# Also update `_run_tool` function and `utils.run_module` in `lsp_runner.py`.
|
||||
result = utils.run_module(
|
||||
module=TOOL_MODULE,
|
||||
argv=argv,
|
||||
use_stdin=use_stdin,
|
||||
cwd=cwd,
|
||||
source=document.source,
|
||||
)
|
||||
except Exception:
|
||||
log_error(traceback.format_exc(chain=True))
|
||||
raise
|
||||
if result.stderr:
|
||||
log_to_output(result.stderr)
|
||||
|
||||
log_to_output(f"{document.uri} :\r\n{result.stdout}")
|
||||
return result
|
||||
|
||||
|
||||
def _run_tool(extra_args: Sequence[str]) -> utils.RunResult:
|
||||
"""Runs tool."""
|
||||
# deep copy here to prevent accidentally updating global settings.
|
||||
settings = copy.deepcopy(_get_settings_by_document(None))
|
||||
|
||||
code_workspace = settings["workspaceFS"]
|
||||
cwd = settings["workspaceFS"]
|
||||
|
||||
use_path = False
|
||||
use_rpc = False
|
||||
if len(settings["path"]) > 0:
|
||||
# 'path' setting takes priority over everything.
|
||||
use_path = True
|
||||
argv = settings["path"]
|
||||
elif len(settings["interpreter"]) > 0 and not utils.is_current_interpreter(
|
||||
settings["interpreter"][0]
|
||||
):
|
||||
# If there is a different interpreter set use JSON-RPC to the subprocess
|
||||
# running under that interpreter.
|
||||
argv = [TOOL_MODULE]
|
||||
use_rpc = True
|
||||
else:
|
||||
# if the interpreter is same as the interpreter running this
|
||||
# process then run as module.
|
||||
argv = [TOOL_MODULE]
|
||||
|
||||
argv += extra_args
|
||||
|
||||
if use_path:
|
||||
# This mode is used when running executables.
|
||||
log_to_output(" ".join(argv))
|
||||
log_to_output(f"CWD Server: {cwd}")
|
||||
result = utils.run_path(argv=argv, use_stdin=True, cwd=cwd)
|
||||
if result.stderr:
|
||||
log_to_output(result.stderr)
|
||||
elif use_rpc:
|
||||
# This mode is used if the interpreter running this server is different from
|
||||
# the interpreter used for running this server.
|
||||
log_to_output(" ".join(settings["interpreter"] + ["-m"] + argv))
|
||||
log_to_output(f"CWD Linter: {cwd}")
|
||||
result = jsonrpc.run_over_json_rpc(
|
||||
workspace=code_workspace,
|
||||
interpreter=settings["interpreter"],
|
||||
module=TOOL_MODULE,
|
||||
argv=argv,
|
||||
use_stdin=True,
|
||||
cwd=cwd,
|
||||
)
|
||||
if result.exception:
|
||||
log_error(result.exception)
|
||||
result = utils.RunResult(result.stdout, result.stderr)
|
||||
elif result.stderr:
|
||||
log_to_output(result.stderr)
|
||||
else:
|
||||
# In this mode the tool is run as a module in the same process as the language server.
|
||||
log_to_output(" ".join([sys.executable, "-m"] + argv))
|
||||
log_to_output(f"CWD Linter: {cwd}")
|
||||
# This is needed to preserve sys.path, in cases where the tool modifies
|
||||
# sys.path and that might not work for this scenario next time around.
|
||||
with utils.substitute_attr(sys, "path", sys.path[:]):
|
||||
try:
|
||||
# TODO: `utils.run_module` is equivalent to running `python -m <pytool-module>`.
|
||||
# If your tool supports a programmatic API then replace the function below
|
||||
# with code for your tool. You can also use `utils.run_api` helper, which
|
||||
# handles changing working directories, managing io streams, etc.
|
||||
# Also update `_run_tool_on_document` function and `utils.run_module` in `lsp_runner.py`.
|
||||
result = utils.run_module(
|
||||
module=TOOL_MODULE, argv=argv, use_stdin=True, cwd=cwd
|
||||
)
|
||||
except Exception:
|
||||
log_error(traceback.format_exc(chain=True))
|
||||
raise
|
||||
if result.stderr:
|
||||
log_to_output(result.stderr)
|
||||
|
||||
log_to_output(f"\r\n{result.stdout}\r\n")
|
||||
return result
|
||||
|
||||
|
||||
# *****************************************************
|
||||
# Logging and notification.
|
||||
# *****************************************************
|
||||
|
||||
+22
-33
@@ -1,39 +1,28 @@
|
||||
from pathlib import Path
|
||||
from lark import Lark, Tree, Token, UnexpectedInput
|
||||
from lark import Lark
|
||||
|
||||
tcl_parser = Lark.open(
|
||||
Path(__file__).parent.joinpath("tcl.lark"), parser="lalr", propagate_positions=True
|
||||
)
|
||||
# Grammar einlesen
|
||||
with open(Path(__file__).parent.joinpath("tcl.lark"), encoding="utf-8") as f:
|
||||
grammar = f.read()
|
||||
|
||||
# LALR-Parser instanziieren
|
||||
parser = Lark(grammar, parser="lalr", propagate_positions=True)
|
||||
|
||||
|
||||
def parse_tcl(source: str):
|
||||
try:
|
||||
tree = tcl_parser.parse(source)
|
||||
return tree, []
|
||||
except UnexpectedInput as e:
|
||||
# e.line, e.column enthalten Position
|
||||
diagnostic = {
|
||||
"range": {
|
||||
"start": {"line": e.line - 1, "character": e.column - 1},
|
||||
"end": {"line": e.line - 1, "character": e.column},
|
||||
},
|
||||
"message": f"Syntaxfehler: {e}",
|
||||
"severity": 1, # Error
|
||||
}
|
||||
return None, [diagnostic]
|
||||
def parse(source: str):
|
||||
return parser.parse(source)
|
||||
|
||||
|
||||
def format_tree(tree):
|
||||
def fmt(node, indent=0):
|
||||
if isinstance(node, Token):
|
||||
return node.value
|
||||
elif isinstance(node, Tree):
|
||||
if node.data == "cmd":
|
||||
parts = []
|
||||
for child in node.children:
|
||||
parts.append(fmt(child, indent))
|
||||
return " ".join(parts) + ";\n"
|
||||
# weitere Node-Typen …
|
||||
return ""
|
||||
|
||||
return "".join(fmt(tree))
|
||||
if __name__ == "__main__":
|
||||
samples = [
|
||||
"set x 42",
|
||||
"set myArray(1) 123",
|
||||
"set myArray2(1,2,3,$string) 123",
|
||||
"proc myProc {} {}",
|
||||
"proc myProc {arg} {}",
|
||||
"proc myProc {arg arg2} {}",
|
||||
"proc myProc {arg {arg2 test}} {}",
|
||||
]
|
||||
for s in samples:
|
||||
tree = parse(s)
|
||||
print(tree.pretty())
|
||||
|
||||
+39
-228
@@ -1,250 +1,61 @@
|
||||
// tcl.lark
|
||||
// Lark grammar for Tcl scripts with proc definitions and optional args
|
||||
|
||||
%import common.CNAME -> IDENTIFIER
|
||||
%import common.INT -> INT
|
||||
%import common.FLOAT -> FLOAT
|
||||
%import common.ESCAPED_STRING -> STRING_LITERAL
|
||||
%import common.WS_INLINE -> WS_INLINE
|
||||
%import common.NEWLINE -> NEWLINE
|
||||
|
||||
// Ignore only spaces, tabs; treat newlines as separators
|
||||
%ignore WS_INLINE
|
||||
// Tcl comments
|
||||
%ignore COMMENT
|
||||
|
||||
start: script
|
||||
script: statement*
|
||||
|
||||
statement: function_declaration
|
||||
| top_level
|
||||
statement: command (";" | NEWLINE)?
|
||||
|
||||
// --- Function declarations (zero or more) ---
|
||||
function_declaration: ("proc" IDENTIFIER "{" function_args "}" "{" function_body "}")*
|
||||
command: set_command
|
||||
| proc_command
|
||||
| r_break
|
||||
| r_continue
|
||||
|
||||
// --- Function arguments (0..n blocks) ---
|
||||
function_args: ("{" IDENTIFIER "}")*
|
||||
proc_command: "proc" IDENTIFIER "{" argument_list "}" body_block
|
||||
|
||||
// --- Function body: zero or more statements ---
|
||||
function_body: (assignment_statement
|
||||
| print_statement
|
||||
| input_statement ";"
|
||||
| if_statement
|
||||
| for_statement
|
||||
| while_statement
|
||||
| switch_statement
|
||||
| return_statement
|
||||
| grouping ";"
|
||||
)*
|
||||
argument_list: [argument (argument)*]
|
||||
|
||||
// --- If / elseif / else in functions ---
|
||||
if_statement: if_header function_body "}" elseif_clause
|
||||
argument: IDENTIFIER -> required_arg
|
||||
| "{" IDENTIFIER (value)? "}" -> optional_arg
|
||||
|
||||
elseif_clause: elseif_header function_body "}" elseif_clause
|
||||
| else_clause
|
||||
|
||||
else_clause: else_header function_body "}"
|
||||
| /* empty */
|
||||
|
||||
// --- Switch in functions ---
|
||||
switch_statement: switch_header case_clause "}"
|
||||
|
||||
case_clause: case_header function_body "}" case_clauses
|
||||
|
||||
case_clauses: case_header function_body "}" case_clauses
|
||||
| default_clause
|
||||
|
||||
default_clause: default_header function_body "}"
|
||||
| /* empty */
|
||||
|
||||
// --- For / While in functions ---
|
||||
for_statement: for_header loop_body_in_function "}"
|
||||
while_statement: while_header loop_body_in_function "}"
|
||||
|
||||
loop_body_in_function: (break_statement
|
||||
| continue_statement
|
||||
| assignment_statement
|
||||
| input_statement ";"
|
||||
| print_statement
|
||||
| return_statement
|
||||
| if_loop_statement
|
||||
| switch_loop_statement
|
||||
| for_statement
|
||||
| while_statement
|
||||
| grouping ";"
|
||||
)*
|
||||
|
||||
if_loop_statement: if_header loop_body_in_function "}" elseif_loop_clause
|
||||
|
||||
elseif_loop_clause: elseif_header loop_body_in_function "}" elseif_loop_clause
|
||||
| else_loop_clause
|
||||
|
||||
else_loop_clause: else_header loop_body_in_function "}"
|
||||
| /* empty */
|
||||
|
||||
switch_loop_statement: switch_header case_loop_clause "}"
|
||||
|
||||
case_loop_clause: case_header loop_body_in_function "}" case_loop_clauses
|
||||
|
||||
case_loop_clauses: case_header loop_body_in_function "}" case_loop_clauses
|
||||
| default_loop_clause
|
||||
|
||||
default_loop_clause: default_header loop_body_in_function "}"
|
||||
| /* empty */
|
||||
|
||||
// --- Top-level (module) statements (zero or more) ---
|
||||
top_level: (assignment_statement
|
||||
| for_main
|
||||
| input_statement ";"
|
||||
| if_main
|
||||
| print_statement
|
||||
| while_main
|
||||
| switch_main
|
||||
| grouping ";"
|
||||
)*
|
||||
|
||||
// --- If / Else at top level ---
|
||||
if_main: if_header main_body "}" elseif_main_clause
|
||||
|
||||
elseif_main_clause: elseif_header main_body "}" elseif_main_clause
|
||||
| else_main_clause
|
||||
|
||||
else_main_clause: else_header main_body "}"
|
||||
| /* empty */
|
||||
|
||||
main_body: (assignment_statement
|
||||
| for_main
|
||||
| input_statement ";"
|
||||
| if_main
|
||||
| print_statement
|
||||
| while_main
|
||||
| switch_main
|
||||
| grouping ";"
|
||||
)*
|
||||
|
||||
// --- Switch at top level ---
|
||||
switch_main: switch_header main_case_clause "}"
|
||||
|
||||
main_case_clause: case_header main_body "}" main_case_clauses
|
||||
|
||||
main_case_clauses: case_header main_body "}" main_case_clauses
|
||||
| default_main_clause
|
||||
|
||||
default_main_clause: default_header main_body "}"
|
||||
| /* empty */
|
||||
|
||||
// --- For / While at top level ---
|
||||
for_main: for_header main_loop_body "}"
|
||||
while_main: while_header main_loop_body "}"
|
||||
|
||||
main_loop_body: (assignment_statement
|
||||
| for_main
|
||||
| input_statement ";"
|
||||
| if_loop_main
|
||||
| print_statement
|
||||
| while_main
|
||||
| switch_loop_main
|
||||
| break_statement
|
||||
| continue_statement
|
||||
| grouping ";"
|
||||
)*
|
||||
|
||||
if_loop_main: if_header main_loop_body "}" elseif_loop_main
|
||||
|
||||
elseif_loop_main: elseif_header main_loop_body "}" elseif_loop_main
|
||||
| else_loop_main
|
||||
|
||||
else_loop_main: else_header main_loop_body "}"
|
||||
| /* empty */
|
||||
|
||||
switch_loop_main: switch_header case_loop_main "}"
|
||||
|
||||
case_loop_main: case_header main_loop_body "}" case_loop_main_clauses
|
||||
|
||||
case_loop_main_clauses: case_header main_loop_body "}" case_loop_main_clauses
|
||||
| default_loop_main
|
||||
|
||||
default_loop_main: default_header main_loop_body "}"
|
||||
| /* empty */
|
||||
|
||||
// --- Primitive statements ---
|
||||
print_statement: "puts" assignment ";"
|
||||
input_statement: "gets" "stdin"
|
||||
assignment_statement: "set" IDENTIFIER index access_value ";"
|
||||
|
||||
grouping: "[" grouping_aux "]"
|
||||
grouping_aux: expression
|
||||
| IDENTIFIER function_param
|
||||
| input_statement
|
||||
| "array" array_aux
|
||||
|
||||
array_aux: "size" IDENTIFIER
|
||||
| "exists" IDENTIFIER
|
||||
|
||||
function_param: "{" param_aux "}"
|
||||
param_aux: assignment_value
|
||||
| expression
|
||||
|
||||
assignment_value: value
|
||||
| "$" IDENTIFIER index
|
||||
| grouping
|
||||
|
||||
index: "(" index_value ")"
|
||||
index_value: value
|
||||
| grouping
|
||||
| "$" IDENTIFIER index
|
||||
|
||||
value: STRING_LITERAL
|
||||
| INT
|
||||
| FLOAT
|
||||
| INT
|
||||
| IDENTIFIER
|
||||
| list
|
||||
|
||||
increment: INT?
|
||||
list: LBRACE [value (WS_INLINE value)*] RBRACE
|
||||
|
||||
break_statement: "break" ";"
|
||||
continue_statement: "continue" ";"
|
||||
return_statement: "return" return_value ";"
|
||||
return_value: assignment_value?
|
||||
body_block: LBRACE body_content RBRACE -> body
|
||||
body_content: ( /[^{}]+/ | body_block )* // allow nested braces
|
||||
|
||||
expr_command: "expr" "{" expression "}"
|
||||
for_init: INT
|
||||
| "$" IDENTIFIER index
|
||||
| expr_command
|
||||
set_command: "set" IDENTIFIER index? value
|
||||
// Index: parentheses with comma-separated items
|
||||
index: "(" index_item ("," index_item)* ")"
|
||||
index_item: INT -> index_int
|
||||
| STRING_VAR -> index_string
|
||||
| "$" IDENTIFIER index? -> nested_index
|
||||
|
||||
// --- Headers for control structures ---
|
||||
if_header: "if" "{" expression "}" "then" "{"
|
||||
elseif_header: "elseif" "{" expression "}" "then" "{"
|
||||
else_header: "else" "{"
|
||||
switch_header: "switch" "$" IDENTIFIER index "{"
|
||||
case_header: "case" INT "{"
|
||||
default_header: "default" "{"
|
||||
for_header: "for" "{" for_declaration "}" "{" expression "}" "{" "incr" IDENTIFIER increment "}" "{"
|
||||
for_declaration: "set" IDENTIFIER for_init
|
||||
while_header: "while" "{" expression "}" "{"
|
||||
|
||||
// --- Expression grammar ---
|
||||
expression: or_expression
|
||||
or_expression: or_expression "||" and_expression
|
||||
| and_expression
|
||||
and_expression: and_expression "&&" equality_expression
|
||||
| equality_expression
|
||||
equality_expression: equality_expression eq_ops relational_expression
|
||||
| relational_expression
|
||||
relational_expression: relational_expression rel_ops additive_expression
|
||||
| additive_expression
|
||||
additive_expression: additive_expression add_ops multiplicative_expression
|
||||
| multiplicative_expression
|
||||
multiplicative_expression: multiplicative_expression mul_ops power_expression
|
||||
| power_expression
|
||||
power_expression: power_expression "**" unary_expression
|
||||
| unary_expression
|
||||
unary_expression: unary_ops unary_expression
|
||||
| term
|
||||
term: "$" IDENTIFIER index
|
||||
| grouping
|
||||
| value
|
||||
| "(" or_expression ")"
|
||||
r_break: "break"
|
||||
r_continue: "continue"
|
||||
|
||||
eq_ops: "eq" | "==" | "ne" | "!="
|
||||
rel_ops: ">" | "<" | ">=" | "<="
|
||||
add_ops: "+" | "-"
|
||||
mul_ops: "*" | "/" | "%"
|
||||
unary_ops: "-" | "!"
|
||||
LBRACE: "{"
|
||||
RBRACE: "}"
|
||||
|
||||
// --- Terminals ---
|
||||
IDENTIFIER: /[a-zA-Z_][a-zA-Z0-9_]*/
|
||||
INT: /-?[0-9]+/
|
||||
FLOAT: /[0-9]+\.[0-9]+/
|
||||
STRING_LITERAL: /"([^"\\]|\\.)*"/
|
||||
STRING_VAR: /(?:[^"\\]|\\.)+/
|
||||
COMMENT: /#[^\r\n]*/
|
||||
|
||||
|
||||
%ignore /[ \t]+/ // spaces & tabs
|
||||
%ignore COMMENT
|
||||
%ignore _NL
|
||||
Reference in New Issue
Block a user