Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion CITATION.cff
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<p align="center"><img src="https://raw.githubusercontent.com/simvia-tech/vs-code-aster/main/media/images/simvia.png" alt="Simvia Logo" width="50%" /></p>

<p align="center">
<a href="/"><img src="https://img.shields.io/badge/version-1.10.0-blue" alt="Version" /></a>
<a href="/"><img src="https://img.shields.io/badge/version-1.10.1-blue" alt="Version" /></a>
<a href="./LICENSE"><img src="https://img.shields.io/badge/license-GPL%203.0-green" alt="License" /></a>
<a href="https://github.com/simvia-tech/vs-code-aster/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/simvia-tech/vs-code-aster/ci.yml?branch=main&label=CI" alt="CI Status" /></a>
<a href="https://github.com/simvia-tech/vs-code-aster/issues"><img src="https://img.shields.io/github/issues/simvia-tech/vs-code-aster?label=issues" alt="GitHub issues" /></a>
Expand Down
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
6 changes: 0 additions & 6 deletions python/lsp/command_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion python/lsp/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def on_initialize(ls: LanguageServer, params: InitializeParams):
"textDocumentSync": 1,
"completionProvider": {
"resolveProvider": False,
"triggerCharacters": ["(", ",", "=", " "],
"triggerCharacters": ["(", ",", "="],
},
"hoverProvider": True,
"definitionProvider": True,
Expand Down
10 changes: 8 additions & 2 deletions python/lsp/managers/completion_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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", "")),
)
)
Expand Down
21 changes: 19 additions & 2 deletions python/lsp/managers/diagnostics_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
find_param,
is_bare_identifier,
required_keywords,
simp_defaults,
types_compatible,
value_in_into,
visible_keywords,
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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", [])
Expand Down
37 changes: 35 additions & 2 deletions python/lsp/managers/hover_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}",
Expand Down Expand Up @@ -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 = <rhs>` and infer a simple Python type.
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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)
Expand Down
22 changes: 22 additions & 0 deletions python/lsp/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
56 changes: 51 additions & 5 deletions src/LspServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 === ' ') {
Expand Down
Loading