tcl_language_support #18

Merged
Christoph merged 10 commits from tcl_language_support into main 2025-07-28 20:17:42 +00:00
5 changed files with 190 additions and 29 deletions
Showing only changes of commit 4d79f268b8 - Show all commits
+124 -13
View File
@@ -12,7 +12,7 @@ import re
import sys
import sysconfig
import traceback
from typing import Any, Optional, Sequence, Tuple
from typing import Any, List, Optional, Sequence, Tuple
import re
@@ -46,27 +46,35 @@ from pygls.workspace.text_document import TextDocument
from common.load_data import standard_items
from common.formatter import format_tcl
from tclint.parser import Parser
from tclint.lexer import TclSyntaxError
from tclint.format import Formatter, FormatterOpts
from tclint.violations import Violation
from tools.semantic_tokens import (
SemanticTokenCollector,
collect_semantic_tokens,
encode_tokens,
)
from tools.nx_plugins import commands
from nx_plugins.poco_plugin import commands
from tools import poco_check
DIAGNOSTIC_SOURCE = "tclint"
class TclLanguageServer(server.LanguageServer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.parser = Parser()
self.parser._commands.update(commands)
self.diagnostics = {}
def format(
self,
document: TextDocument,
options: lsp.FormattingOptions,
range: Optional[Tuple[int, int]] = None,
):
parser = Parser()
parser._commands.update(commands)
tree = parser.parse(document.source)
log_to_output(tree.pretty(2))
# parser = Parser(command_plugins=["nx_plugins.poco_plugin.py"])
# parser._commands.update(commands)
indent = "\t" if not options.insert_spaces else " " * options.tab_size
@@ -81,9 +89,87 @@ class TclLanguageServer(server.LanguageServer):
if range is not None:
start, end = range
return formatter.format_partial(document.source[start:end], parser)
return formatter.format_partial(document.source[start:end], self.parser)
return formatter.format_top(document.source, parser)
return formatter.format_top(document.source, self.parser)
def linter(
self,
document: TextDocument,
) -> List[Violation]:
violations = []
tree = self.parser.parse(document.source)
violations += self.parser.violations
# if debug > 0:
# print(tree.pretty(positions=(debug > 1)))
for checker in poco_check.get_checkers():
violations += checker.check(document.source, tree)
# v = CommentVisitor()
# ignore_lines = v.run(tree, path)
# violations = filter_violations(violations, config.ignore, ignore_lines)
return violations
def lint(self, document: TextDocument):
diagnostics = []
try:
violations = self.linter(document)
except TclSyntaxError as e:
return [
lsp.Diagnostic(
message=str(e),
severity=lsp.DiagnosticSeverity.Error,
range=lsp.Range(
start=lsp.Position(e.start[0] - 1, e.start[1] - 1),
end=lsp.Position(e.end[0] - 1, e.end[1] - 1),
),
code="syntax error",
source=DIAGNOSTIC_SOURCE,
)
]
for violation in violations:
message = violation.message
severity = lsp.DiagnosticSeverity.Warning
start = lsp.Position(
line=violation.start[0] - 1, character=violation.start[1] - 1
)
end = lsp.Position(
line=violation.end[0] - 1, character=violation.end[1] - 1
)
diagnostics.append(
lsp.Diagnostic(
message=message,
severity=severity,
range=lsp.Range(
start=start,
end=end,
),
code=violation.id,
source=DIAGNOSTIC_SOURCE,
)
)
return diagnostics
def _compute_diagnostics(self, document: TextDocument) -> List[lsp.Diagnostic]:
return self.lint(document)
def compute_diagnostics(self, document: TextDocument):
# `None` sentinel ensures that `diagnostics` gets updated if the URI is not
# present.
_, previous = self.diagnostics.get(document, (0, None))
diagnostics = self._compute_diagnostics(document)
# Only update if the list has changed
if previous != diagnostics:
self.diagnostics[document.uri] = (document.version, diagnostics)
WORKSPACE_SETTINGS = {}
@@ -126,6 +212,7 @@ TOOL_ARGS = [] # default arguments always passed to your tool.
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)
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DID_SAVE)
@@ -142,7 +229,33 @@ def did_close(params: lsp.DidCloseTextDocumentParams) -> None:
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DID_CHANGE)
def did_change(params: lsp.DidChangeTextDocumentParams) -> None:
"""LSP handler for textDocument/didChange request"""
log_to_output("Document has changed")
document = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
LSP_SERVER.compute_diagnostics(document)
@LSP_SERVER.feature(
lsp.TEXT_DOCUMENT_DIAGNOSTIC,
lsp.DiagnosticOptions(
identifier="pull-diagnostics",
inter_file_dependencies=False,
workspace_diagnostics=False,
),
)
def document_diagnostic(params: lsp.DocumentDiagnosticParams):
"""Return diagnostics for the requested document"""
was_cached = True
if (uri := params.text_document.uri) not in LSP_SERVER.diagnostics:
was_cached = False
doc = LSP_SERVER.workspace.get_text_document(uri)
LSP_SERVER.compute_diagnostics(doc)
version, diagnostics = LSP_SERVER.diagnostics[uri]
result_id = f"{uri}@{version}"
if was_cached and result_id == params.previous_result_id:
return lsp.UnchangedDocumentDiagnosticReport(result_id)
return lsp.FullDocumentDiagnosticReport(items=diagnostics, result_id=result_id)
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_COMPLETION)
@@ -271,9 +384,7 @@ def formatting(params: lsp.DocumentFormattingParams) -> list[lsp.TextEdit] | Non
# **********************************************************
@LSP_SERVER.feature(lsp.WORKSPACE_DID_CHANGE_CONFIGURATION)
def did_change_configuration(params: lsp.DidChangeConfigurationParams):
settings = params.settings
log_to_output(str(settings))
"""LSP Handler for Config Changes"""
@LSP_SERVER.feature(lsp.INITIALIZE)
View File
+13
View File
@@ -0,0 +1,13 @@
from tclint.commands.checks import CommandArgError
def _lib_ge_command_buffer(args, parser):
if len(args) != 4:
raise CommandArgError(
f"wrong # of args to LIB_GE_command_buffer_edit_*: got {len(args)}, expected 4"
)
args[2] = parser.parse_script(args[2])
return args
commands = {"LIB_GE_command_buffer_edit_prepend": _lib_ge_command_buffer}
-16
View File
@@ -1,16 +0,0 @@
from tclint.syntax_tree import BracedWord
def lib_spf_prepend(args, parser):
if len(args) >= 5:
# Heuristik: myTag wurde zu früh getrennt, hänge ihn an den BracedBlock
merged_contents = args[3].contents + "\n" + args[4].contents
merged = BracedWord(merged_contents, pos=args[3].pos, end_pos=args[4].end_pos)
script = parser.parse_script(merged)
return args[:3] + [script] # [routine, arg1, arg2, script]
elif len(args) >= 4:
args[2] = parser.parse_script(args[2])
return args
commands = {"LIB_SPF_prepend": lib_spf_prepend}
+53
View File
@@ -0,0 +1,53 @@
from enum import Enum
from tclint.syntax_tree import Visitor
from tclint.violations import Violation
class PoCoRule(Enum):
POCO_VALIDATION = "poco-validation"
def __str__(self):
return self.value
class PocoCommandChecker(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):
name = command.routine.contents
if name not in {"LIB_GE_command_buffer_edit_prepend"}:
return
if len(command.args) != 4:
self._violations.append(
Violation(
PoCoRule.POCO_VALIDATION,
f"{name}: expected 4 arguments, got {len(command.args)}",
command.pos,
command.end_pos,
)
)
return
script_arg = command.args[2]
if script_arg.contents is None:
self._violations.append(
Violation(
PoCoRule.POCO_VALIDATION,
f"{name}: third argument must be a braced script",
script_arg.pos,
script_arg.end_pos,
)
)
def get_checkers():
checkers = (PocoCommandChecker(),)
return checkers