Compare commits

...
27 Commits
Author SHA1 Message Date
Christoph aeebb5c964 fix: some highlight error
/ build_and_publish (release) Successful in 38s
2025-08-11 21:35:14 +02:00
Christoph f659db1147 Update package.json 2025-08-11 15:12:55 +00:00
Christoph b059a75205 Update version to 2025.9.001 2025-08-11 14:24:17 +00:00
Christoph 27e2b3bf6e Merge pull request 'update' (#25) from tcl_language_support into main
/ build_and_publish (release) Successful in 33s
Reviewed-on: #25
2025-08-09 21:35:09 +00:00
Christoph f55c3ba904 Update version to 2025.8.200 2025-08-09 12:09:07 +00:00
Christoph cb430a5553 Update version to 2025.8.2
/ build_and_publish (release) Successful in 33s
2025-08-09 12:06:50 +00:00
Christoph 637a28ad82 Update version to 2025.08.1
/ build_and_publish (release) Successful in 43s
2025-08-09 11:57:08 +00:00
Christoph 499df9b51d Update README.md
/ build_and_publish (release) Successful in 35s
2025-08-09 11:55:59 +00:00
Christoph Brandau 31ccae9a40 update 2025-08-08 09:18:22 +02:00
Christoph 9b373e85b7 Update version to 2025.08.101 2025-08-07 05:35:45 +00:00
Christoph 0c5f4ea6fb Merge pull request 'tcl_language_support' (#24) from tcl_language_support into main
/ build_and_publish (release) Successful in 47s
Reviewed-on: #24
2025-08-07 05:34:22 +00:00
Christoph Brandau 793b35f39f Merge branch 'tcl_language_support' of https://git.cbsk-tech.de/Christoph/nx_post_support into tcl_language_support 2025-08-07 07:27:09 +02:00
Christoph Brandau 3780fc27dc mod Readme 2025-08-07 07:27:08 +02:00
Christoph 6541830ef6 Update version to 2025.8.100 2025-08-06 20:10:08 +00:00
Christoph 509b13e99f add icon
/ build_and_publish (release) Successful in 46s
2025-08-06 22:08:39 +02:00
Christoph 396cfdc4ec Update version to 0.5.0 2025-08-06 18:22:29 +00:00
Christoph ccd22e4343 Update .gitea/workflows/build_and_puplish.yml
/ build_and_publish (release) Successful in 41s
2025-08-06 18:21:06 +00:00
Christoph 7d83ae1012 Update package.json 2025-08-06 18:19:22 +00:00
Christoph 029eaebd57 Update version to 0.5.0-beta.1 2025-08-06 18:13:14 +00:00
Christoph 2a2db8f217 Merge pull request 'tcl_language_support' (#23) from tcl_language_support into main
/ build_and_publish (push) Failing after 34s
Reviewed-on: #23
2025-08-06 18:09:48 +00:00
Christoph 8be9aae05d update pipline 2025-08-06 20:09:15 +02:00
Christoph Brandau 6bded783a1 add functions to completion 2025-08-06 15:26:53 +02:00
Christoph f5c61267e3 read all procs from psc file 2025-08-05 21:45:22 +02:00
Christoph Brandau b680c01989 update file sourcing 2025-08-05 18:18:17 +02:00
Christoph 1abf89fe56 load all files for completion 2025-08-04 21:11:20 +02:00
Christoph Brandau 6a051a6dee del test file 2025-08-04 16:06:50 +02:00
Christoph d22d897d23 Update version to 0.4.14 2025-08-04 12:52:21 +00:00
17 changed files with 486 additions and 483 deletions
+9 -4
View File
@@ -1,7 +1,6 @@
on:
push:
tags:
- "*"
release:
types: [published]
jobs:
build_and_publish:
runs-on: ubuntu-latest
@@ -35,7 +34,13 @@ jobs:
git add package.json
git commit -m "Update version to ${{ github.ref_name }}"
git push origin HEAD:main
- name: Publish to Visual Studio Marketplace
- name: Publish to Visual Studio Marketplace (Pre-Release)
if: ${{ github.event.release.prerelease }}
run: vsce publish --pre-release
env:
VSCE_PAT: ${{ secrets.VSCODE_MARKETPALCE }}
- name: Publish to Visual Studio Marketplace (Stable)
if: ${{ !github.event.release.prerelease }}
run: vsce publish
env:
VSCE_PAT: ${{ secrets.VSCODE_MARKETPALCE }}
+2 -1
View File
@@ -2,6 +2,7 @@
"ruff.configuration": {
"lint": {
"extend-ignore": ["F821", "E402"]
}
},
"line-length": 200
}
}
+49
View File
@@ -0,0 +1,49 @@
# NX Postprocessor Support
A comprehensive VS Code extension providing language support for NX CAD postprocessor development, including CDL (Cutter Definition Language), TCL, and DEF files.
## Features
- **Syntax Highlighting** - Full syntax highlighting for CDL, TCL, and DEF files
- **Code Formatting** - Automatic code formatting for all supported file types
- **Multi-language Support** - Supports NX CDL, TCL, and DEF file formats
- **Intelligent Code Analysis** - Linting and error detection for postprocessor code
- **Auto-completion** - Context-aware code completion for faster development
## Supported File Types
- `.cdl` - NX Cutter Definition Language files
- `.tcl` - NX TCL postprocessor files
- `.def` - NX Definition files
## Installation
1. Install from the VS Code Marketplace
2. Open any `.cdl`, `.tcl`, or `.def` file
3. The extension will automatically activate and provide language support
## Configuration
The extension can be configured through VS Code settings:
- `nx-post-support.interpreter` - Specify custom Python interpreter path for the language server
- `nx-post-support.formatter` - Enable/disable the TCL formatter (default: false)
- `nx-post-support.inlayHint` - Enable/disable inlay Hints (default: true)
## Usage
Simply open any supported file type and enjoy:
- Syntax highlighting
- Error detection and linting
- Code completion
- Code formatting (Format Document command)
- Hover information
## Contributing
This extension is actively maintained. For issues or feature requests, please visit our [repository](https://git.cbsk-tech.de/Christoph/nx_post_support.git).
## License
MIT License - see LICENSE file for details.
+6 -3
View File
@@ -79,7 +79,8 @@ export async function getWorkspaceSettings(
interpreter: resolveVariables(interpreter, workspace),
importStrategy: config.get<string>(`importStrategy`) ?? "useBundled",
showNotifications: config.get<string>(`showNotifications`) ?? "off",
formatter: config.get<boolean>(`formatter`) ?? true
formatter: config.get<boolean>(`formatter`) ?? true,
inlayHint: config.get<boolean>(`inlayHint`) ?? true
}
return workspaceSetting
}
@@ -111,7 +112,8 @@ export async function getGlobalSettings(
interpreter: interpreter,
importStrategy: getGlobalValue<string>(config, "importStrategy", "useBundled"),
showNotifications: getGlobalValue<string>(config, "showNotifications", "off"),
formatter: config.get<boolean>(`formatter`) ?? true
formatter: config.get<boolean>(`formatter`) ?? true,
inlayHint: config.get<boolean>(`inlayHint`) ?? true
}
return setting
}
@@ -126,7 +128,8 @@ export function checkIfConfigurationChanged(
`${namespace}.interpreter`,
`${namespace}.importStrategy`,
`${namespace}.showNotifications`,
`${namespace}.formatter`
`${namespace}.formatter`,
`${namespace}.inlayHint`
]
const changed = settings.map((s) => e.affectsConfiguration(s))
return changed.includes(true)
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

+7 -1
View File
@@ -2,8 +2,9 @@
"name": "nx-post-support",
"displayName": "NX Postprocessor Support",
"description": "",
"version": "0.4.13",
"version": "2025.9.0",
"publisher": "Christoph",
"icon": "images/nx-1.png",
"serverInfo": {
"name": "NX Postprocessor Support",
"module": "nx-post-support"
@@ -84,6 +85,11 @@
"default": false,
"description": "Use the TCL formatter from `NX Postprocessor Support`"
},
"nx-post-support.inlayHint": {
"type": "boolean",
"default": true,
"description": "Use the Inlay Hints in from `NX Postprocessor Support`"
},
"nx-post-support.importStrategy": {
"default": "useBundled",
"description": "Defines where `NX Postprocessor Support` is imported from.",
+1 -1
View File
@@ -32,7 +32,7 @@ if debugger_path:
# This will ensure that execution is paused as soon as the debugger
# connects to VS Code. If you don't want to pause here comment this
# line and set breakpoints as appropriate.
debugpy.breakpoint()
# debugpy.breakpoint()
SERVER_PATH = os.fspath(pathlib.Path(__file__).parent / "lsp_server.py")
# NOTE: Set breakpoint in `lsp_server.py` before continuing.
+4
View File
@@ -71,6 +71,10 @@
{
"label": "expr",
"kind": "keyword"
},
{
"label": "return",
"kind": "keyword"
}
],
"MOM_procs": [
+56 -151
View File
@@ -38,124 +38,13 @@ update_sys_path(
# pylint: disable=wrong-import-position,import-error
import lsp_jsonrpc as jsonrpc
import lsprotocol.types as lsp
from pygls import server, uris, workspace
from pygls.workspace.text_document import TextDocument
from pygls import uris, workspace
from common.load_data import standard_items
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.completion_items import completion, remove_existing_items, remove_shared_keys
from tools.inlay_hint import InlayHintGenerator
DIAGNOSTIC_SOURCE = "nx-post-support"
class TclLanguageServer(server.LanguageServer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.parser = parser.CustomParser()
for command in commands:
self.parser._commands.update(command)
self.diagnostics = {}
def format(
self,
document: TextDocument,
options: lsp.FormattingOptions,
range: Optional[Tuple[int, int]] = None,
):
# parser = Parser(command_plugins=["nx_plugins.poco_plugin.py"])
# parser._commands.update(commands)
indent = "\t" if not options.insert_spaces else " " * options.tab_size
formatter = Formatter(
FormatterOpts(
indent=indent,
spaces_in_braces=False,
max_blank_lines=500,
indent_namespace_eval=True,
),
)
if range is not None:
start, end = range
return formatter.format_partial(document.source[start:end], self.parser)
return formatter.format_top(document.source, self.parser)
def linter(
self,
document: TextDocument,
) -> List[Violation]:
violations = []
self.parser.violations = []
tree = self.parser.parse(document.source)
violations += self.parser.violations
# log_to_output(tree.pretty(2))
for checker in checks.get_checkers():
violations += checker.check(document.source, tree)
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)
from tools.file_sourcing import get_all_psc_files, read_psc_file
from lsp_tclserver import TclLanguageServer
WORKSPACE_SETTINGS = {}
@@ -164,9 +53,7 @@ RUNNER = pathlib.Path(__file__).parent / "lsp_runner.py"
MAX_WORKERS = 5
LSP_SERVER = TclLanguageServer(
name="NX Postprocessor Support", version="0.0.1", max_workers=MAX_WORKERS
)
LSP_SERVER = TclLanguageServer(name="NX Postprocessor Support", version="0.0.1", max_workers=MAX_WORKERS)
# **********************************************************
# Tool specific code goes below this.
@@ -189,9 +76,6 @@ 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)
@@ -210,9 +94,7 @@ def did_change(params: lsp.DidChangeTextDocumentParams) -> None:
"""LSP handler for textDocument/didChange 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.update_poco_completion_for_file(document)
@LSP_SERVER.feature(
@@ -243,32 +125,38 @@ def document_diagnostic(params: lsp.DocumentDiagnosticParams):
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_COMPLETION)
def on_completion(params: lsp.CompletionParams) -> list[lsp.CompletionItem]:
_ = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
poco = []
items = (
standard_items.tcl_keyword_list
+ standard_items.nx_procs
+ standard_items.nx_variables
+ completion.custom_functions
)
for key, value in LSP_SERVER.poco_completion.items():
poco.extend(value)
items = standard_items.tcl_keyword_list + standard_items.nx_procs + standard_items.nx_variables + poco
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)
# return []
# ast = LSP_SERVER.parser.parse(doc.source)
# symbols = LSP_SERVER.extract_tcl_symbols(ast)
# return symbols
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_INLAY_HINT)
def inlay_hints(params: lsp.InlayHintParams):
log_to_output(str(GLOBAL_SETTINGS.get("inlayHint", False)))
if not GLOBAL_SETTINGS.get("inlayHint", False):
return []
document = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
tree = LSP_SERVER.parser.parse(document.source)
# collect Inlay Hints
generator = InlayHintGenerator(completion.proc_signatures)
tree.accept(generator, recurse=True)
return generator.hints
hints = []
for key, value in LSP_SERVER.proc_signatures.items():
generator = InlayHintGenerator(LSP_SERVER.proc_signatures[key])
tree.accept(generator, recurse=True)
hints += generator.hints
return hints
@LSP_SERVER.feature(
@@ -283,7 +171,7 @@ def semantic_tokens(params: lsp.SemanticTokensParams):
data = []
plugins = []
hl = _Highlighter(plugins, log_to_output=log_to_output)
hl = _Highlighter(plugins, LSP_SERVER.poco_completion)
tree = LSP_SERVER.parser.parse(document.source)
tree.accept(hl, recurse=True)
@@ -336,9 +224,7 @@ def hover(params: lsp.HoverParams) -> lsp.Hover:
label = match.get("label", "")
parameters = match.get("parameters", [])
param_lines = (
"\n".join(f"- `{p['name']}`: {p['desc']}" for p in parameters) or "_None_"
)
param_lines = "\n".join(f"- `{p['name']}`: {p['desc']}" for p in parameters) or "_None_"
example_data = match.get("example", [])
example_md = "\n".join(f"{line}" for line in example_data)
@@ -422,12 +308,8 @@ def initialize(params: lsp.InitializeParams) -> lsp.InitializeResult:
settings = params.initialization_options["settings"]
_update_workspace_settings(settings)
log_to_output(
f"Settings used to run Server:\r\n{json.dumps(settings, indent=4, ensure_ascii=False)}\r\n"
)
log_to_output(
f"Global settings:\r\n{json.dumps(GLOBAL_SETTINGS, indent=4, ensure_ascii=False)}\r\n"
)
log_to_output(f"Settings used to run Server:\r\n{json.dumps(settings, indent=4, ensure_ascii=False)}\r\n")
log_to_output(f"Global settings:\r\n{json.dumps(GLOBAL_SETTINGS, indent=4, ensure_ascii=False)}\r\n")
semantic_tokens_legend = lsp.SemanticTokensLegend(
token_types=TOKEN_TYPES,
token_modifiers=TokenModifier,
@@ -435,13 +317,37 @@ def initialize(params: lsp.InitializeParams) -> lsp.InitializeResult:
return lsp.InitializeResult(
capabilities=lsp.ServerCapabilities(
document_formatting_provider=GLOBAL_SETTINGS.get("formatter", True),
semantic_tokens_provider=lsp.SemanticTokensOptions(
legend=semantic_tokens_legend, full=True, range=False
),
semantic_tokens_provider=lsp.SemanticTokensOptions(legend=semantic_tokens_legend, full=True, range=False),
)
)
@LSP_SERVER.feature(lsp.INITIALIZED)
def initialized(params: lsp.InitializedParams):
root = LSP_SERVER.workspace.root_path
psc_files = get_all_psc_files(pathlib.Path(root))
for psc_file in psc_files:
poco_files = read_psc_file(psc_file)
for sourced_layer in poco_files:
completion.reset()
try:
file_root = pathlib.Path(root).joinpath(sourced_layer.subfolder if sourced_layer.subfolder else "")
for tcl_file in sourced_layer.files:
filepath = pathlib.Path(file_root).joinpath(f"{tcl_file}.tcl")
if not filepath.exists():
continue
completion.reset()
document = LSP_SERVER.workspace.get_text_document(filepath.as_uri()) # filepath.read_text(encoding="utf-8")
tree = LSP_SERVER.parser.parse(document.source)
tree.accept(completion, recurse=True)
remove_existing_items(completion.custom_functions, LSP_SERVER.poco_completion)
LSP_SERVER.poco_completion[str(filepath)] = completion.custom_functions
remove_shared_keys(LSP_SERVER.proc_signatures, completion.proc_signatures)
LSP_SERVER.proc_signatures[str(filepath)] = completion.proc_signatures
except Exception as e:
log_to_output(f"Fehler beim Parsen von {filepath}: {e}")
@LSP_SERVER.feature(lsp.EXIT)
def on_exit(_params: Optional[Any] = None) -> None:
"""Handle clean up on exit."""
@@ -462,6 +368,7 @@ def _get_global_defaults():
"importStrategy": GLOBAL_SETTINGS.get("importStrategy", "useBundled"),
"showNotifications": GLOBAL_SETTINGS.get("showNotifications", "off"),
"formatter": GLOBAL_SETTINGS.get("formatter", True),
"inlayHint": GLOBAL_SETTINGS.get("inlayHint", True),
}
@@ -533,9 +440,7 @@ def _get_settings_by_document(document: workspace.Document | None):
# *****************************************************
# Logging and notification.
# *****************************************************
def log_to_output(
message: str, msg_type: lsp.MessageType = lsp.MessageType.Log
) -> None:
def log_to_output(message: str, msg_type: lsp.MessageType = lsp.MessageType.Log) -> None:
LSP_SERVER.show_message_log(message, msg_type)
+139
View File
@@ -0,0 +1,139 @@
import logging
import pathlib
from typing import List, Optional, Tuple
import lsprotocol.types as lsp
from pygls.workspace.text_document import TextDocument
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 pygls import server, uris
from tools.completion_items import completion, remove_existing_items, remove_shared_keys
DIAGNOSTIC_SOURCE = "nx-post-support"
class TclLanguageServer(server.LanguageServer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.parser = parser.CustomParser()
for command in commands:
self.parser._commands.update(command)
self.diagnostics = {}
self.poco_completion: dict = {}
self.proc_signatures: dict = {}
def update_poco_completion_for_file(self, document: TextDocument):
"""Update poco_completion for a specific file when it changes"""
filepath = str(pathlib.Path(uris.to_fs_path(document.uri)))
# Remove existing completion items for this file
if filepath in self.poco_completion:
del self.poco_completion[filepath]
if filepath in self.proc_signatures:
del self.proc_signatures[filepath]
# Parse and extract new completion items
completion.reset()
try:
tree = self.parser.parse(document.source)
tree.accept(completion, recurse=True)
remove_existing_items(completion.custom_functions, self.poco_completion)
self.poco_completion[filepath] = completion.custom_functions
remove_shared_keys(self.proc_signatures, completion.proc_signatures)
self.proc_signatures[filepath] = completion.proc_signatures
except Exception as e:
logging.debug(f"Error parsing {filepath}: {e}")
def format(
self,
document: TextDocument,
options: lsp.FormattingOptions,
range: Optional[Tuple[int, int]] = None,
):
# parser = Parser(command_plugins=["nx_plugins.poco_plugin.py"])
# parser._commands.update(commands)
indent = "\t" if not options.insert_spaces else " " * options.tab_size
formatter = Formatter(
FormatterOpts(
indent=indent,
spaces_in_braces=False,
max_blank_lines=500,
indent_namespace_eval=True,
),
)
if range is not None:
start, end = range
return formatter.format_partial(document.source[start:end], self.parser)
return formatter.format_top(document.source, self.parser)
def linter(
self,
document: TextDocument,
) -> List[Violation]:
violations = []
self.parser.violations = []
tree = self.parser.parse(document.source)
violations += self.parser.violations
for checker in checks.get_checkers():
violations += checker.check(document.source, tree)
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)
-78
View File
@@ -1,78 +0,0 @@
from tclint.parser import Parser as tcLintParser
from tclint.lexer import Lexer as tclingLexer
def main():
parser = tcLintParser(True)
tree = parser.parse("""puts hello
proc myProc {arg {arg7 0}} {}
set myVar 123
puts puts
LIB_SPF_prepend MOM_strt Start_Lib {
set somthing 1
set more 2
} myTag
LIB_SPF_prepend MOM_strt Start_Lib {
proc test {} {
puts "Hello"
}
set somthing 1
set someting 3
} myTag""")
print(tree.pretty(2))
def lexer_test():
lexer = tclingLexer()
tree = lexer.input("""
if {$oem(custom_clamp_4th) == 1 && $oem(custom_clamp_5th) == 1 \\
&& $oem(status_clamp_4th) == "off" && $oem(status_clamp_5th) == "off"}""")
# print("Lexing input:\n", code)
# print("\nTokens:\n" + "-" * 30)
while lexer.type() is not None:
tok_type = lexer.type()
tok_value = lexer.value()
tok_pos = lexer.pos()
print(f"Type: {tok_type:20} | Value: {repr(tok_value):20} | Pos: {tok_pos}")
lexer.next()
def test_1():
from tclint.lexer import Lexer, TOK_BACKSLASH_NEWLINE
code = "expr {1 == 2 \\\n&& 3 == 4}"
lexer = Lexer()
lexer.input(code)
while lexer.type() is not None:
print(
f"Type: {lexer.type():<20} | Value: {lexer.value()!r} | Pos: {lexer.pos()}"
)
lexer.next()
class NodeVisitor:
def visit_script(self, node):
for stmt in node.statements:
stmt.accept(self)
def visit_proc(self, node):
print(f"Proc: {node.name}, args={node.args}")
def visit_set(self, node):
print(f"Set: {node.varname} = {node.value}")
def visit_namespace(self, node):
print(f"Namespace: {node.name}")
def visit_command(self, node):
print(f"Command: {node.name} {node.args}")
if __name__ == "__main__":
test_1()
+42 -8
View File
@@ -1,5 +1,6 @@
from tclint.syntax_tree import Visitor, Command, BareWord, List
import lsprotocol.types as lsp
from common.load_data import standard_items
class CompletionItems:
@@ -38,14 +39,18 @@ class _Completion(Visitor):
if routine.contents == "proc" and command.args:
first_arg = command.args[0]
if hasattr(first_arg, "value") and not any(
item.label == first_arg.value for item in self._custom_functions
):
self._custom_functions.append(
lsp.CompletionItem(
label=first_arg.value, kind=lsp.CompletionItemKind.Function
)
)
if not first_arg.value:
return
if any(item.label == first_arg.value for item in standard_items.nx_procs):
return
try:
self._custom_functions.remove(first_arg.value)
except ValueError:
pass
self._custom_functions.append(lsp.CompletionItem(label=first_arg.value, kind=lsp.CompletionItemKind.Function))
if len(command.args) < 2:
return
@@ -65,4 +70,33 @@ class _Completion(Visitor):
self._proc_signatures[first_arg.value] = param_names
def remove_existing_items(items: list[lsp.CompletionItem], store: dict) -> None:
"""
Entfernt alle CompletionItems aus dem store, deren label in der items-Liste vorkommt.
Änderungen erfolgen in-place.
"""
labels_to_remove = {item.label for item in items}
for key in list(store.keys()):
filtered = [ci for ci in store[key] if ci.label not in labels_to_remove]
if filtered:
store[key] = filtered
else:
del store[key]
def remove_shared_keys(nested_dict: dict[str, dict[str, list]], flat_dict: dict[str, list]) -> None:
"""
Entfernt alle Keys aus nested_dict[file][func], wenn func auch in flat_dict vorhanden ist.
Änderungen erfolgen in-place.
"""
for file_path, func_dict in list(nested_dict.items()):
for func_name in list(func_dict.keys()):
if func_name in flat_dict:
del nested_dict[file_path][func_name]
if not nested_dict[file_path]:
del nested_dict[file_path]
completion = _Completion()
+51
View File
@@ -0,0 +1,51 @@
import xml.etree.ElementTree as ET
from dataclasses import dataclass
from typing import List, Optional
from pathlib import Path
@dataclass
class SourcedFile:
layer_name: str
subfolder: Optional[str]
files: List[str]
def read_psc_file(psc_file: Path) -> List[SourcedFile]:
tree = ET.parse(psc_file)
root = tree.getroot()
layers = root.findall(".//Layer")
layer_info_list: List[SourcedFile] = []
for layer in layers:
layer_name = layer.attrib.get("Name")
subfolder = layer.attrib.get("SubFolder")
# Scripts-Filenames
scripts = layer.find("Scripts")
script_names = []
if scripts is not None:
for filename in scripts.findall("Filename"):
name = filename.attrib.get("Name")
if name:
script_names.append(name)
layer_info_list.append(
SourcedFile(layer_name=layer_name, subfolder=subfolder, files=script_names)
)
return layer_info_list
def get_all_psc_files(root_path: Path) -> list[Path]:
return [path for path in root_path.rglob("*.psc")]
if __name__ == "__main__":
test = get_all_psc_files(
Path(
r"H:\janus-engineering-customers\KSB_Frankenthal\custom\library\machine\installed_machines\ksb_pe_grob_g550_sone\postprocessor"
)
)
print(test)
for pp in test:
read_psc_file(pp)
View File
+15 -22
View File
@@ -5,6 +5,7 @@ from tclint.commands import get_commands
import attrs
from common.load_data import standard_items
from tools.completion_items import completion
import lsprotocol.types as lsp
class TokenModifier(enum.IntFlag):
@@ -40,10 +41,10 @@ TOKEN_TYPES = [
class _Highlighter(Visitor):
def __init__(self, plugins, log_to_output):
def __init__(self, plugins, custom_functions: dict[str : list[lsp.CompletionItem]]):
self._commands = get_commands(plugins)
self._tokens = []
self.log_to_output = log_to_output
self.custom_functions = custom_functions
def _get_token_info(self, node):
"""Hilfsmethode um Token-Informationen aus verschiedenen Node-Typen zu extrahieren."""
@@ -58,11 +59,7 @@ class _Highlighter(Visitor):
# CompoundBareWord: versuche erstes Segment
if hasattr(node, "children") and node.children:
first_segment = node.children[0]
if (
hasattr(first_segment, "value")
and first_segment.value is not None
and hasattr(first_segment, "pos")
):
if hasattr(first_segment, "value") and first_segment.value is not None and hasattr(first_segment, "pos"):
line, col = first_segment.pos
return (line - 1, col - 1), len(first_segment.value)
@@ -84,13 +81,15 @@ class _Highlighter(Visitor):
pass
def visit_bare_word(self, word: BareWord):
if any(item.label == word.value for item in standard_items.nx_procs) or any(
item.label == word.value for item in completion.custom_functions
):
name = word.value
in_standard = any(item.label == name for item in standard_items.nx_procs)
in_custom = any(item.label == name for items in self.custom_functions.values() for item in items)
if in_standard or in_custom:
line, col = word.pos
self._tokens.append(
(((line - 1, col - 1), len(word.value), "function", []))
)
self._tokens.append((((line - 1, col - 1), len(name), "function", [])))
def visit_command(self, command: Command):
routine = command.routine
@@ -158,9 +157,7 @@ class _Highlighter(Visitor):
# Parameter mit Default-Wert ist meist eine List (z.B. {arg default})
elif hasattr(child, "children") and len(child.children) >= 1:
name_node = child.children[0]
if hasattr(name_node, "value") and hasattr(
name_node, "pos"
):
if hasattr(name_node, "value") and hasattr(name_node, "pos"):
line, col = name_node.pos
self._tokens.append(
(
@@ -174,9 +171,7 @@ class _Highlighter(Visitor):
first_arg = command.args[1]
if hasattr(first_arg, "pos") and first_arg.value is not None:
line, col = first_arg.pos
self._tokens.append(
(((line - 1, col - 1), len(first_arg.value), "class", []))
)
self._tokens.append((((line - 1, col - 1), len(first_arg.value), "class", [])))
def tokens(self) -> list[Token]:
"""Encode tokens as described in
@@ -185,9 +180,7 @@ class _Highlighter(Visitor):
tokens = []
last_line = 0
last_col = 0
for (line, col), length, tok_type, tok_modifier in sorted(
self._tokens, key=lambda x: x[0]
):
for (line, col), length, tok_type, tok_modifier in sorted(self._tokens, key=lambda x: x[0]):
line_delta = line - last_line
col_delta = col
if line == last_line:
+46
View File
@@ -0,0 +1,46 @@
import logging
from collections import defaultdict
from typing import List, DefaultDict, Union
from tclint.syntax_tree import Visitor, Command, CommandSub, Node, Script
class SymbolTable:
"""Holds a symbol table (links symbols to nodes)."""
def __init__(self):
self.proc_def: DefaultDict[str, list[Node]] = defaultdict(list)
def add_proc_definition(self, command: Command) -> None:
"""Add definition of procedure"""
# command holds the "proc" keyword, so the proc name is 1st argument
proc_name_node = command.args[0]
proc_name = proc_name_node.contents
if not proc_name:
return
logging.debug(
f"Definition of proc '{proc_name}' at {proc_name_node._pos_str()}"
)
self.proc_def[proc_name].append(proc_name_node)
def lookup_proc_definitions(self, symbol_text: str) -> List[Node]:
"""Lookup definitions of the procedure pointed at by node"""
if symbol_text is None or symbol_text not in self.proc_def:
return []
return self.proc_def[symbol_text]
class SymbolTableBuilder(Visitor):
"""Builds a symbol table."""
def __init__(self):
self.table = SymbolTable()
def build(self, tree: Union[CommandSub, Script]) -> SymbolTable:
"""Run the builder visitor through the syntax tree, building a table."""
tree.accept(self, recurse=True)
return self.table
def visit_command(self, command: Command) -> None:
if command.routine.contents == "proc":
self.table.add_proc_definition(command)
+59 -214
View File
@@ -1,30 +1,19 @@
{
"fileTypes": [
"tcl"
],
"fileTypes": [ "tcl" ],
"name": "tcl",
"patterns": [
{
"include": "#main"
}
{ "include": "#main" }
],
"scopeName": "source.tcl",
"uuid": "c7017136-2ff2-48e9-bdb0-570cf238b4a2",
"repository": {
"main": {
"patterns": [
{
"include": "#command"
}
]
"patterns": [ { "include": "#command" } ]
},
"args": {
"patterns": [
{
"include": "#quoted_args"
},
{
"include": "#numeric"
},
{ "include": "#quoted_args" },
{ "include": "#numeric" },
{
"match": "((?<=;)\\s*#.*$)",
"name": "comment.tcl",
@@ -36,22 +25,10 @@
"patterns": [
{
"begin": "((?<!\\\\)\\{)",
"beginCaptures": {
"1": {
"name": "punctuation.tcl"
}
},
"patterns": [
{
"include": "#braced__1"
}
],
"end": "(\\}([^\\s\\]]*))",
"endCaptures": {
"1": {
"name": "punctuation.tcl"
}
}
"beginCaptures": { "1": { "name": "punctuation.tcl" } },
"patterns": [ { "include": "#braced__1" } ],
"end": "(\\}([^\\s\\]\\x{0022}]*))",
"endCaptures": { "1": { "name": "punctuation.tcl" } }
}
]
},
@@ -61,42 +38,24 @@
"match": "(\\\\(?:\\d{1,3}|x[a-fA-F0-9]{1,2}|u[a-fA-F0-9]{1,4}|U[a-fA-F0-9]{1,8}|.))",
"name": "constant.character.escape.tcl"
},
{
"include": "#keywords"
},
{ "include": "#keywords" },
{
"match": "((?<={)\\s*(?:after|append|apply|array|auto_execok|auto_import|auto_load|auto_mkindex|auto_qualify|auto_reset|bgerror|binary|break|catch|cd|chan|clock|close|concat|continue|coroutine|dde|dict|encoding|eof|error|eval|exec|exit|expr|fblocked|fconfigure|fcopy|fileevent|file|flush|foreach|for|format|gets|global|glob|history|http|if|incr|info|interp|join|lappend|lassign|lindex|linsert|list|llength|lmap|load|lrange|lrepeat|lreplace|lreverse|lsearch|lset|lsort|memory|my|namespace|nextto|next|oo::class|oo::copy|oo::define|oo::objdefine|oo::object|open|package|parray|pid|pkg::create|pkg_mkIndex|platform::shell|proc|puts|pwd|read|regexp|registry|regsub|rename|return|scan|seek|self|set|socket|source|split|string|subst|switch|tailcall|tcl::prefix|tcl_endOfWord|tcl_findLibrary|tcl_startOfNextWord|tcl_startOfPreviousWord|tcl_wordBreakAfter|tcl_wordBreakBefore|tell|throw|time|trace|try|unknown|unload|unset|update|uplevel|upvar|variable|vwait|while|yieldto|yield)\\s+)",
"name": "keyword.tcl",
"comment": "Special handling for known commands."
},
{
"include": "#braced_inner"
},
{
"include": "#args"
}
{ "include": "#braced_inner" },
{ "include": "#args" }
]
},
"braced_inner": {
"patterns": [
{
"begin": "(\\{)",
"beginCaptures": {
"1": {
"name": "punctuation.tcl"
}
},
"patterns": [
{
"include": "#braced_inner__1"
}
],
"beginCaptures": { "1": { "name": "punctuation.tcl" } },
"patterns": [ { "include": "#braced_inner__1" } ],
"end": "(\\})",
"endCaptures": {
"1": {
"name": "punctuation.tcl"
}
}
"endCaptures": { "1": { "name": "punctuation.tcl" } }
}
]
},
@@ -106,15 +65,9 @@
"match": "(\\\\[\\x{007b}\\x{007d}])",
"name": "constant.character.escape.tcl"
},
{
"include": "#numeric"
},
{
"include": "#braced_inner"
},
{
"include": "#args"
}
{ "include": "#numeric" },
{ "include": "#braced_inner" },
{ "include": "#args" }
]
},
"braced_lit": {
@@ -139,22 +92,10 @@
"patterns": [
{
"begin": "(\\\\\\s*$)",
"beginCaptures": {
"1": {
"name": "constant.character.escape.tcl"
}
},
"patterns": [
{
"include": "#command__1"
}
],
"beginCaptures": { "1": { "name": "constant.character.escape.tcl" } },
"patterns": [ { "include": "#command__1" } ],
"end": "((?<!\\\\\\s)$)",
"endCaptures": {
"1": {
"name": "none.tcl"
}
}
"endCaptures": { "1": { "name": "none.tcl" } }
},
{
"match": "(^\\s*#.*)",
@@ -169,73 +110,41 @@
{
"match": "(?<=^|{)(\\s*proc\\s+)(\\S+\\s+)((?:(?<!\\\\){)(?:[^\\x{007b}\\x{007d}\\n]*(?:\\g<1>(?:\\\\{|\\\\}|[^\\x{007b}\\x{007d}])*)*)})",
"captures": {
"1": {
"name": "keyword.tcl"
},
"2": {
"name": "entity.name.function.tcl"
},
"3": {
"name": "none.tcl"
}
"1": { "name": "keyword.tcl" },
"2": { "name": "entity.name.function.tcl" },
"3": { "name": "none.tcl" }
},
"comment": "Proc command."
},
{ "include": "#regexp" },
{ "include": "#keywords" },
{
"include": "#regexp"
},
{
"include": "#keywords"
},
{
"match": "(^\\s*\\S+|(?:(?<=[^\\x{005c}]\\[)[^\\s\\]]++(?!\\])))",
"match": "(^\\s*[^\\s\\x{0022}]+|(?:(?<=[^\\x{005c}]\\[)[^\\s\\]]+))",
"name": "keyword.tcl",
"comment": "All other commands. (NOTE: Iro doesn't support possessive quantifiers, but the grammar does; be sure to replace the second + with ++ after regenerating!)"
},
{
"include": "#args"
}
{ "include": "#args" }
]
},
"command__1": {
"patterns": [
{
"include": "#keywords"
},
{
"include": "#args"
}
{ "include": "#keywords" },
{ "include": "#args" }
]
},
"embedded": {
"patterns": [
{
"begin": "((?<!\\\\)\\[)",
"beginCaptures": {
"1": {
"name": "punctuation.tcl"
}
},
"patterns": [
{
"include": "#embedded__1"
}
],
"beginCaptures": { "1": { "name": "punctuation.tcl" } },
"patterns": [ { "include": "#embedded__1" } ],
"end": "((?<!\\\\)\\])",
"endCaptures": {
"1": {
"name": "punctuation.tcl"
}
}
"endCaptures": { "1": { "name": "punctuation.tcl" } }
}
]
},
"embedded__1": {
"patterns": [
{
"include": "#command"
}
]
"patterns": [ { "include": "#command" } ]
},
"keywords": {
"patterns": [
@@ -263,30 +172,16 @@
"patterns": [
{
"begin": "((?<!\\\\)\")",
"beginCaptures": {
"1": {
"name": "string.tcl"
}
},
"patterns": [
{
"include": "#quoted__1"
}
],
"beginCaptures": { "1": { "name": "string.tcl" } },
"patterns": [ { "include": "#quoted__1" } ],
"end": "(\")",
"endCaptures": {
"1": {
"name": "string.tcl"
}
}
"endCaptures": { "1": { "name": "string.tcl" } }
}
]
},
"quoted__1": {
"patterns": [
{
"include": "#quoted_args"
},
{ "include": "#quoted_args" },
{
"match": "([^\\x{0024}\\x{005b}\\x{0022}\\x{005c}]+)",
"name": "string.tcl"
@@ -295,22 +190,14 @@
},
"quoted_args": {
"patterns": [
{
"include": "#braced"
},
{
"include": "#quoted"
},
{
"include": "#embedded"
},
{ "include": "#braced" },
{ "include": "#quoted" },
{ "include": "#embedded" },
{
"match": "(\\\\(?:\\d{1,3}|x[a-fA-F0-9]{1,2}|u[a-fA-F0-9]{1,4}|U[a-fA-F0-9]{1,8}|.))",
"name": "constant.character.escape.tcl"
},
{
"include": "#variable"
}
{ "include": "#variable" }
]
},
"regexp": {
@@ -318,57 +205,27 @@
{
"begin": "((?<=^|[\\[\\x{007b}\\x{003b}])\\s*(?:regexp|regsub)\\s+)((?:(?:(?:-about|-all|-expanded|-indices|-inline|-lineanchor|-linestop|-line|-nocase|(?:-index\\s+(?:end|\\d+)(?:[\\+\\-]-?\\d+)?))\\s+)*(?:--\\s+)?)?)",
"beginCaptures": {
"1": {
"name": "keyword.tcl"
},
"2": {
"name": "none.tcl"
}
"1": { "name": "keyword.tcl" },
"2": { "name": "none.tcl" }
},
"patterns": [
{
"include": "#regexp__1"
}
],
"patterns": [ { "include": "#regexp__1" } ],
"end": "(\\s+)",
"endCaptures": {
"1": {
"name": "none.tcl"
}
}
"endCaptures": { "1": { "name": "none.tcl" } }
},
{
"include": "#args"
}
{ "include": "#args" }
]
},
"regexp__1": {
"patterns": [
{
"include": "#braced_lit_re"
},
{ "include": "#braced_lit_re" },
{
"begin": "((?:(?<!\\\\)\"))",
"beginCaptures": {
"1": {
"name": "string.regexp.tcl"
}
},
"patterns": [
{
"include": "#regexp__2"
}
],
"beginCaptures": { "1": { "name": "string.regexp.tcl" } },
"patterns": [ { "include": "#regexp__2" } ],
"end": "((?:(?<!\\\\)\"))",
"endCaptures": {
"1": {
"name": "string.regexp.tcl"
}
}
},
{
"include": "#variable"
"endCaptures": { "1": { "name": "string.regexp.tcl" } }
},
{ "include": "#variable" },
{
"match": "(\\S+)",
"name": "string.regexp.tcl",
@@ -378,15 +235,9 @@
},
"regexp__2": {
"patterns": [
{
"include": "#braced_lit_re"
},
{
"include": "#embedded"
},
{
"include": "#variable"
},
{ "include": "#braced_lit_re" },
{ "include": "#embedded" },
{ "include": "#variable" },
{
"match": "(\\\\\"|[^\\x{0022}])",
"name": "string.regexp.tcl",
@@ -404,15 +255,9 @@
},
"word_lit__1": {
"patterns": [
{
"include": "#braced_lit"
},
{
"include": "#embedded"
},
{
"include": "#variable"
},
{ "include": "#braced_lit" },
{ "include": "#embedded" },
{ "include": "#variable" },
{
"match": "(\\\\\"|[^\\x{0022}])",
"name": "string.tcl",