diff --git a/README.md b/README.md index 2ac6c67..3e8f8c4 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,6 @@ A comprehensive VS Code extension providing language support for NX CAD postproc ## Features - **Syntax Highlighting** - Full syntax highlighting for CDL, TCL, and DEF files -- **Language Server Integration** - Powered by `tclint` for advanced language features - **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 diff --git a/client/src/common/settings.ts b/client/src/common/settings.ts index c919394..8263532 100644 --- a/client/src/common/settings.ts +++ b/client/src/common/settings.ts @@ -79,7 +79,8 @@ export async function getWorkspaceSettings( interpreter: resolveVariables(interpreter, workspace), importStrategy: config.get(`importStrategy`) ?? "useBundled", showNotifications: config.get(`showNotifications`) ?? "off", - formatter: config.get(`formatter`) ?? true + formatter: config.get(`formatter`) ?? true, + inlayHint: config.get(`inlayHint`) ?? true } return workspaceSetting } @@ -111,7 +112,8 @@ export async function getGlobalSettings( interpreter: interpreter, importStrategy: getGlobalValue(config, "importStrategy", "useBundled"), showNotifications: getGlobalValue(config, "showNotifications", "off"), - formatter: config.get(`formatter`) ?? true + formatter: config.get(`formatter`) ?? true, + inlayHint: config.get(`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) diff --git a/package.json b/package.json index e92cf50..83a2b30 100644 --- a/package.json +++ b/package.json @@ -85,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.", @@ -124,4 +129,4 @@ "prettier": "^3.4.2", "typescript": "^5.7.2" } -} +} \ No newline at end of file diff --git a/server/src/common/completion_list.json b/server/src/common/completion_list.json index ce9021b..687e40e 100644 --- a/server/src/common/completion_list.json +++ b/server/src/common/completion_list.json @@ -71,6 +71,10 @@ { "label": "expr", "kind": "keyword" + }, + { + "label": "return", + "kind": "keyword" } ], "MOM_procs": [ diff --git a/server/src/lsp_server.py b/server/src/lsp_server.py index 27d8d65..4ce9378 100644 --- a/server/src/lsp_server.py +++ b/server/src/lsp_server.py @@ -38,123 +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, remove_existing_items, remove_shared_keys from tools.inlay_hint import InlayHintGenerator from tools.file_sourcing import get_all_psc_files, read_psc_file - -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 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 lsp_tclserver import TclLanguageServer WORKSPACE_SETTINGS = {} @@ -204,6 +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) + LSP_SERVER.update_poco_completion_for_file(document) @LSP_SERVER.feature( @@ -253,6 +144,9 @@ def on_completion(params: lsp.CompletionParams) -> list[lsp.CompletionItem]: @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) @@ -474,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), } diff --git a/server/src/lsp_tclserver.py b/server/src/lsp_tclserver.py new file mode 100644 index 0000000..69370c4 --- /dev/null +++ b/server/src/lsp_tclserver.py @@ -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)