From e9d2433b82a54738ee9ea0d06cc98db9898cb6c3 Mon Sep 17 00:00:00 2001 From: Ulysse Bouchet Date: Tue, 28 Apr 2026 10:14:57 +0200 Subject: [PATCH 1/3] fix: eliminate spurious diagnostics on nested CO() and BLOC-gated keywords Stop bailing out of command parsing when an inner line looks like a top-level call (e.g. `MODELE=CO("name")`), seed BLOC-evaluation context with SIMP defaults so commands like CALC_MODES expose their gated keywords, and treat `CO("name")` as a future-output declaration that registers `name` as a defined variable. --- python/lsp/command_registry.py | 6 ------ python/lsp/managers/diagnostics_manager.py | 21 +++++++++++++++++++-- python/lsp/validators.py | 22 ++++++++++++++++++++++ 3 files changed, 41 insertions(+), 8 deletions(-) diff --git a/python/lsp/command_registry.py b/python/lsp/command_registry.py index 3116880..ef20233 100644 --- a/python/lsp/command_registry.py +++ b/python/lsp/command_registry.py @@ -673,12 +673,6 @@ def _find_command_end(self, lines: list[str], start_idx: int, start_char_pos: in line = lines[i] line_clean = self._remove_inline_comment(line) - # Check for new command - if i > start_idx: - new_cmd = self._find_command_start(line) - if new_cmd is not None: - return {"end_line": i, "complete": False} - in_string = False string_char = None diff --git a/python/lsp/managers/diagnostics_manager.py b/python/lsp/managers/diagnostics_manager.py index 03af45b..4800e1a 100644 --- a/python/lsp/managers/diagnostics_manager.py +++ b/python/lsp/managers/diagnostics_manager.py @@ -26,6 +26,7 @@ find_param, is_bare_identifier, required_keywords, + simp_defaults, types_compatible, value_in_into, visible_keywords, @@ -98,6 +99,19 @@ def _validate(self, doc_uri: str) -> list[Diagnostic]: var_index[ci.var_name] = (ci.start_line, ci.name) except Exception: continue + # `CO("name")` inside a macro declares a future output bound + # to `name`. Register those so later references resolve. + try: + end = ci.end_line if ci.end_line is not None else ci.zone_end + start_idx = max(0, ci.start_line - 1) + end_idx = min(len(doc.lines), end) + body = "\n".join(doc.lines[start_idx:end_idx]) + for m in re.finditer(r"\bCO\s*\(\s*['\"]([A-Za-z_]\w*)['\"]", body): + name = m.group(1) + if name not in var_index: + var_index[name] = (ci.start_line, ci.name) + except Exception: + pass for ci in registry.commands.values(): try: @@ -139,11 +153,14 @@ def _validate_command( except Exception: pairs = [] - context = {} try: - context = ci.parsed_params.copy() + context = simp_defaults(cmd_obj.definition) except Exception: context = {} + try: + context.update(ci.parsed_params or {}) + except Exception: + pass try: cmd_def_params = self.core.get_command_def(ci.name).get("params", []) diff --git a/python/lsp/validators.py b/python/lsp/validators.py index 635210d..0503a65 100644 --- a/python/lsp/validators.py +++ b/python/lsp/validators.py @@ -85,6 +85,28 @@ def required_keywords(definition, context): continue +def simp_defaults(definition) -> dict: + """Collect ``{name: defaut}`` for top-level SIMP keywords that declare a + `defaut`. Used to seed BLOC-evaluation context with values the user + didn't type but the catalog assumes.""" + out: dict = {} + try: + for key, kwd in definition.items(): + if not hasattr(kwd, "definition"): + continue + if _is_bloc(kwd) or _is_factor(kwd): + continue + try: + d = kwd.definition.get("defaut") + except Exception: + d = None + if d is not None: + out[key] = d + except Exception: + return out + return out + + def find_param(params: list[dict], name: str) -> dict | None: """Find a parsed-param dict (the shape produced by `Catalogs.parse_kwd`) by name, descending into BLOC children.""" From 6b7456e038d2bbe74de36b0302eb3baed8f4308a Mon Sep 17 00:00:00 2001 From: Ulysse Bouchet Date: Tue, 28 Apr 2026 11:05:05 +0200 Subject: [PATCH 2/3] fix: stop suggest widget from auto-opening on top-level newlines Match popSuggest triggers exactly so accepted snippet bodies don't re-fire the widget, gate the newline retrigger on being inside an unclosed call so plain Enter at top level no longer latches VS Code on a "No suggestions" session, drop the space trigger character that was causing the same latch on auto-indent, and pair DEBUT autocompletion with a matching FIN() block with the cursor in between. --- python/lsp/handlers.py | 2 +- python/lsp/managers/completion_manager.py | 10 +++- src/LspServer.ts | 56 +++++++++++++++++++++-- 3 files changed, 60 insertions(+), 8 deletions(-) diff --git a/python/lsp/handlers.py b/python/lsp/handlers.py index 83547cd..a7eb08f 100644 --- a/python/lsp/handlers.py +++ b/python/lsp/handlers.py @@ -74,7 +74,7 @@ def on_initialize(ls: LanguageServer, params: InitializeParams): "textDocumentSync": 1, "completionProvider": { "resolveProvider": False, - "triggerCharacters": ["(", ",", "=", " "], + "triggerCharacters": ["(", ",", "="], }, "hoverProvider": True, "definitionProvider": True, diff --git a/python/lsp/managers/completion_manager.py b/python/lsp/managers/completion_manager.py index ed103a5..f7725ec 100644 --- a/python/lsp/managers/completion_manager.py +++ b/python/lsp/managers/completion_manager.py @@ -139,13 +139,19 @@ def _completion(self, doc_uri: str, position) -> CompletionList: def _suggest_commands(self) -> CompletionList: items = [] for cmd in self.core.get_CATA_commands(): + if cmd["name"] == "DEBUT": + insert = "DEBUT()\n$0\nFIN()" + retrigger = None + else: + insert = cmd["name"] + "($0)" + retrigger = _retrigger_command() items.append( CompletionItem( label=cmd["name"], kind=CompletionItemKind.Function, - insert_text=cmd["name"] + "($0)", + insert_text=insert, insert_text_format=InsertTextFormat.Snippet, - command=_retrigger_command(), + command=retrigger, documentation=_md(cmd.get("doc", "")), ) ) diff --git a/src/LspServer.ts b/src/LspServer.ts index cde9c20..b3fbaf3 100644 --- a/src/LspServer.ts +++ b/src/LspServer.ts @@ -16,6 +16,42 @@ import { getCatalogChannel, reconcileCatalogCache, } from './CatalogResolver'; +/** + * Crude paren-balance check: are we inside an unclosed `(` at this position? + * Skips string literals and `#` comments. Good enough to distinguish "inside + * a function call" from "top level" without round-tripping to the LSP. + */ +function isInsideCall(doc: vscode.TextDocument, pos: vscode.Position): boolean { + let depth = 0; + let inString: string | null = null; + for (let line = 0; line <= pos.line; line++) { + const text = doc.lineAt(line).text; + const max = line === pos.line ? pos.character : text.length; + for (let i = 0; i < max; i++) { + const c = text[i]; + if (inString) { + if (c === inString && text[i - 1] !== '\\') { + inString = null; + } + continue; + } + if (c === '"' || c === "'") { + inString = c; + continue; + } + if (c === '#') { + break; + } + if (c === '(') { + depth++; + } else if (c === ')') { + depth = Math.max(0, depth - 1); + } + } + } + return depth > 0; +} + /** * Singleton class to manage the Python LSP client for Code-Aster. * Handles client creation, start, restart, notifications, and editor listeners. @@ -189,21 +225,31 @@ export class LspServer { setTimeout(() => vscode.commands.executeCommand('editor.action.triggerSuggest'), 0); }; - if (typed.includes('(')) { + // Use exact equality (not `includes`) so multi-char insertions like + // accepted snippets — whose body may contain `(`, `\n`, etc. — don't + // re-fire the suggest widget on every accept. + if (typed === '(' || typed === '()') { vscode.commands.executeCommand('editor.action.triggerParameterHints'); popSuggest(); return; } - if (typed.includes(',')) { + if (typed === ',') { popSuggest(); return; } - if (typed.includes('=')) { + if (typed === '=') { popSuggest(); return; } - if (typed.includes('\n')) { - popSuggest(); + if (typed === '\n') { + // Only re-open the popup if Enter was pressed inside an unbalanced + // call. At top level a brand-new line has no useful suggestions and + // an empty server response would latch VS Code's "No suggestions" + // session — quickSuggestions on the first letter typed will fire a + // fresh request and open the popup naturally. + if (isInsideCall(editor.document, editor.selection.active)) { + popSuggest(); + } return; } if (typed === ' ') { From 8eb04850eefa890e7531f5674ae63fb19fade08a Mon Sep 17 00:00:00 2001 From: Ulysse Bouchet Date: Tue, 28 Apr 2026 13:31:23 +0200 Subject: [PATCH 3/3] chore: bump version to 1.10.1 Wraps the diagnostics false-positive fixes, the suggest-widget polish, the DEBUT/FIN snippet pairing, and a hover for CO-declared variables. --- CHANGELOG.md | 16 ++++++++++++ CITATION.cff | 2 +- README.md | 2 +- ROADMAP.md | 2 +- package-lock.json | 4 +-- package.json | 2 +- python/lsp/managers/hover_manager.py | 37 ++++++++++++++++++++++++++-- 7 files changed, 57 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45aa238..228a367 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,22 @@ All notable changes to the **VS Code Aster** extension will be documented in thi The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.10.1] - 2026-04-28 + +A round of LSP polish: completion no longer auto-opens on top-level newlines or sticks on "No suggestions", and edit-time diagnostics stop crying wolf on macros that take `CO("name")` outputs or commands whose keywords live behind a `BLOC` gate. + +### Added + +- `DEBUT` autocompletion now inserts a paired `FIN()` block with the cursor parked between them. +- Hovering a `CO("name")`-declared variable shows which macro will produce it and on what line, matching the regular variable hover. + +### Fixed + +- Suggest widget no longer auto-opens on a brand-new top-level line, no longer latches on accepted snippet bodies, and still pops parameter suggestions on Enter inside a function call. +- `CO("name")` inside a macro is recognised as a future-output declaration, so later references to `name` no longer flag as undefined. +- `ASSE_ELEM_SSD` and similar commands no longer report `SOUS_STRUC` / `LIAISON` missing when the keyword's value contains another call (`MODELE=CO(...)`). +- `CALC_MODES` keywords gated by `BLOC(condition=...)` on defaulted catalog values are no longer reported as unknown. + ## [1.10.0] - 2026-04-27 A broad LSP and IDE-experience pass: cave-driven catalog resolution, a TypeScript-style hover layer, context-aware autocompletion, edit-time diagnostics with quick fixes, a `.comm` formatter, a guided setup flow, and a new activity-bar panel that doubles as a command dictionary. The status bar slims to an icon and Output channels are grouped under a single `code_aster:` prefix. diff --git a/CITATION.cff b/CITATION.cff index 94cbb12..f1d8788 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -1,4 +1,4 @@ -cff-version: 1.10.0 +cff-version: 1.10.1 title: VS Code Aster message: >- If you use this software, please cite it using the diff --git a/README.md b/README.md index aed1d12..3e933e2 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@

Simvia Logo

- Version + Version License CI Status GitHub issues diff --git a/ROADMAP.md b/ROADMAP.md index 7db88dc..472677c 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -4,7 +4,7 @@ The extension aims to reduce friction between modeling, validation, execution, and analysis by bringing **code_aster** native workflows into the editor. -## Current Capabilities (v1.10.0) +## Current Capabilities (v1.10.1) - `.export` file generator - 3D mesh viewer diff --git a/package-lock.json b/package-lock.json index dfe04fb..afbb530 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "vs-code-aster", - "version": "1.10.0", + "version": "1.10.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "vs-code-aster", - "version": "1.10.0", + "version": "1.10.1", "license": "GPL-3.0", "dependencies": { "@kitware/vtk.js": "^35.10.0", diff --git a/package.json b/package.json index aa58fcd..1868d41 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vs-code-aster", "displayName": "VS Code Aster", - "version": "1.10.0", + "version": "1.10.1", "description": "VS Code extension for code_aster", "publisher": "simvia", "license": "GPL-3.0", diff --git a/python/lsp/managers/hover_manager.py b/python/lsp/managers/hover_manager.py index 78a03e2..dd80aeb 100644 --- a/python/lsp/managers/hover_manager.py +++ b/python/lsp/managers/hover_manager.py @@ -64,6 +64,10 @@ def _lang() -> str: "en": "Assigned by `{cmd}` at line {line}", "fr": "Assigné par `{cmd}` à la ligne {line}", }, + "declared_by_co": { + "en": "Declared via `CO(...)` inside `{cmd}` at line {line}", + "fr": "Déclaré via `CO(...)` dans `{cmd}` à la ligne {line}", + }, "assigned_at_line": { "en": "Assigned at line {line}", "fr": "Assigné à la ligne {line}", @@ -205,6 +209,12 @@ def display(self, doc_uri, position): if assignment is not None: return _hover(_render_variable_reference(word, assignment, cata)) + # `CO("name")` inside a macro body declares `name` as a future + # output. Treat it like a regular assignment for hover purposes. + co_info = _nearest_co_declaration(registry, doc.lines, word, position.line + 1) + if co_info is not None: + return _hover(_render_variable_reference(word, co_info, cata, via_co=True)) + # (1b) Plain Python assignments (e.g. `TempRef = 20.0`) aren't # tracked by CommandRegistry. Scan the doc for the nearest preceding # line-level `WORD = ` and infer a simple Python type. @@ -229,6 +239,28 @@ def _escape_italic(text: str) -> str: # ---------- variable-reference helper ------------------------------------- +def _nearest_co_declaration(registry, doc_lines, var_name: str, cursor_line: int): + """Find the nearest preceding command whose body contains a + `CO("var_name")` declaration. Returns the CommandInfo of the macro + that will produce `var_name` as one of its outputs.""" + try: + pattern = re.compile(r"\bCO\s*\(\s*['\"]" + re.escape(var_name) + r"['\"]") + except re.error: + return None + best = None + for info in registry.commands.values(): + if info.start_line > cursor_line: + continue + end = info.end_line if info.end_line is not None else info.zone_end + start_idx = max(0, info.start_line - 1) + end_idx = min(len(doc_lines), end) + body = "\n".join(doc_lines[start_idx:end_idx]) + if pattern.search(body): + if best is None or info.start_line > best.start_line: + best = info + return best + + def _nearest_assignment(registry, var_name: str, cursor_line: int): """Walk all tracked commands; return the CommandInfo whose `var_name` matches and whose assignment line is on or before the cursor. Prefers @@ -244,7 +276,7 @@ def _nearest_assignment(registry, var_name: str, cursor_line: int): return best -def _render_variable_reference(name: str, info, cata) -> str: +def _render_variable_reference(name: str, info, cata, via_co: bool = False) -> str: cmd_obj = cata.get_command_obj(info.name) if info.name else None type_str = _return_type_hint(cmd_obj) if cmd_obj else None header = f"{name}: {type_str}" if type_str else name @@ -254,7 +286,8 @@ def _render_variable_reference(name: str, info, cata) -> str: out.append(header) out.append("```") out.append("") - out.append("*" + _escape_italic(_t("assigned_by", cmd=info.name, line=info.start_line)) + "*") + label_key = "declared_by_co" if via_co else "assigned_by" + out.append("*" + _escape_italic(_t(label_key, cmd=info.name, line=info.start_line)) + "*") # Footer still points at the command that produced it. if info.name: _append_doc_link(out, info.name)