From f570a7cb46cc6dd1f9583aa080de985ff5b0791b Mon Sep 17 00:00:00 2001 From: Ulysse Bouchet Date: Wed, 15 Apr 2026 14:47:57 +0200 Subject: [PATCH 1/4] feat: open .med files directly in the mesh viewer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Register a CustomReadonlyEditorProvider for .med/.mmed/.rmed so clicking the file in the explorer opens the mesh viewer in full view instead of the binary-file text editor warning. The webview now renders a dedicated error state when the med→obj conversion fails. --- package.json | 12 ++ src/MedEditorProvider.ts | 110 ++++++++++++++++++ src/WebviewVisu.ts | 74 +++++++++--- src/extension.ts | 5 + .../components/layout/LoadingScreen.svelte | 20 ++-- webviews/viewer/src/lib/state.ts | 1 + webviews/viewer/src/main.ts | 6 +- 7 files changed, 204 insertions(+), 24 deletions(-) create mode 100644 src/MedEditorProvider.ts diff --git a/package.json b/package.json index 57a32c5..4a1ed8b 100644 --- a/package.json +++ b/package.json @@ -125,6 +125,18 @@ "path": "./syntaxes/comm.tmLanguage.json" } ], + "customEditors": [ + { + "viewType": "vs-code-aster.medViewer", + "displayName": "MED Mesh Viewer", + "selector": [ + { "filenamePattern": "*.med" }, + { "filenamePattern": "*.mmed" }, + { "filenamePattern": "*.rmed" } + ], + "priority": "default" + } + ], "keybindings": [ { "command": "editor.action.commentLine", diff --git a/src/MedEditorProvider.ts b/src/MedEditorProvider.ts new file mode 100644 index 0000000..4f072d5 --- /dev/null +++ b/src/MedEditorProvider.ts @@ -0,0 +1,110 @@ +/** + * Custom editor provider for MED mesh files. + * + * Registers as the default editor for `.med`, `.mmed`, `.rmed` via the + * `customEditors` contribution in package.json. When VS Code opens one of + * those files, it calls `resolveCustomEditor` and hands us a WebviewPanel, + * which we populate with the same mesh viewer used by the .comm flow. + * + * This bypasses the "binary file" message that VS Code would otherwise show + * when the user clicks a .med file. + */ +import * as vscode from 'vscode'; +import * as path from 'path'; +import { getObjFiles, readObjFilesContent } from './VisuManager'; +import { WebviewVisu } from './WebviewVisu'; +import { sendTelemetry, TelemetryType } from './telemetry'; + +/** + * MED extensions that are routed to the custom editor via the static + * `customEditors` selector in package.json. + * + * User-configured numeric extensions (e.g. `.71`, `.72`) are not listed here + * and are handled by the dynamic interception in `extension.ts`. + */ +export const STATIC_MED_EXTS = new Set(['.med', '.mmed', '.rmed']); + +export class MedEditorProvider implements vscode.CustomReadonlyEditorProvider { + public static readonly viewType = 'vs-code-aster.medViewer'; + + /** Active standalone viewers, keyed by the MED file's fsPath. */ + private activeViewers: Map = new Map(); + + private constructor(private readonly extensionRootDir: string) {} + + /** + * Register the provider with VS Code. Returns a Disposable that should be + * pushed onto `context.subscriptions`. + */ + public static register(context: vscode.ExtensionContext): vscode.Disposable { + const provider = new MedEditorProvider(context.extensionUri.fsPath); + return vscode.window.registerCustomEditorProvider(MedEditorProvider.viewType, provider, { + webviewOptions: { + retainContextWhenHidden: true, + }, + supportsMultipleEditorsPerDocument: false, + }); + } + + async openCustomDocument( + uri: vscode.Uri, + _openContext: vscode.CustomDocumentOpenContext, + _token: vscode.CancellationToken + ): Promise { + return { uri, dispose: () => {} }; + } + + async resolveCustomEditor( + document: vscode.CustomDocument, + webviewPanel: vscode.WebviewPanel, + _token: vscode.CancellationToken + ): Promise { + const medUri = document.uri; + const medPath = medUri.fsPath; + const medBaseName = path.basename(medPath, path.extname(medPath)); + + // Build the viewer with an empty initial payload so the loading screen + // appears immediately. Real data is sent via `sendInit` after conversion. + const visu = new WebviewVisu( + MedEditorProvider.viewType, + this.extensionRootDir, + 'webviews/viewer/dist/index.html', + [], + [], + undefined, + medBaseName, + webviewPanel + ); + + this.activeViewers.set(medPath, visu); + webviewPanel.onDidDispose(() => { + this.activeViewers.delete(medPath); + }); + + void sendTelemetry(TelemetryType.VIEWER_OPENED); + + try { + const objUris = await getObjFiles([medPath]); + + if (objUris.length === 0) { + webviewPanel.webview.postMessage({ + type: 'error', + body: { + message: `Failed to convert ${path.basename(medPath)} to a viewable format.`, + }, + }); + return; + } + + const fileContexts = await readObjFilesContent(objUris); + const objFilenames = objUris.map((uri) => path.basename(uri.fsPath)); + visu.sendInit(fileContexts, objFilenames); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + webviewPanel.webview.postMessage({ + type: 'error', + body: { message }, + }); + } + } +} diff --git a/src/WebviewVisu.ts b/src/WebviewVisu.ts index 4676bc8..5b40ab9 100644 --- a/src/WebviewVisu.ts +++ b/src/WebviewVisu.ts @@ -12,6 +12,9 @@ export class WebviewVisu implements vscode.Disposable { private objects?: string[]; private selectedGroups: string[]; + private readyReceived = false; + private deferredInit?: { fileContexts: string[]; objFilenames: string[] }; + public get webview(): vscode.Webview { return this.panel.webview; } @@ -32,6 +35,9 @@ export class WebviewVisu implements vscode.Disposable { * @param fileContexts The file contexts to send to the webview. * @param objFilenames File names for mesh data. * @param viewColumn The column in which to show the webview. + * @param title Optional tab title. + * @param existingPanel Optional pre-existing panel (e.g. provided by a CustomEditorProvider). + * If provided, `viewColumn` is ignored and the panel is reused instead of creating a new one. */ public constructor( viewType: string, @@ -40,7 +46,8 @@ export class WebviewVisu implements vscode.Disposable { fileContexts: string[], objFilenames: string[], viewColumn?: vscode.ViewColumn, - title?: string + title?: string, + existingPanel?: vscode.WebviewPanel ) { viewColumn = viewColumn || vscode.ViewColumn.Beside; @@ -60,8 +67,17 @@ export class WebviewVisu implements vscode.Disposable { title = this.extractHtmlTitle(htmlFileContent, 'Visualizer'); } - // Create webview panel with icon - this.panel = vscode.window.createWebviewPanel(viewType, title, viewColumn, options); + if (existingPanel) { + // Reuse a panel supplied by VS Code (CustomEditorProvider flow) + this.panel = existingPanel; + this.panel.webview.options = { + enableScripts: true, + localResourceRoots: [vscode.Uri.file(resourceRootDir)], + }; + this.panel.title = title; + } else { + this.panel = vscode.window.createWebviewPanel(viewType, title, viewColumn, options); + } this.panel.iconPath = { light: vscode.Uri.file(path.join(resourceRootDir, 'media', 'icons', '3d.svg')), dark: vscode.Uri.file(path.join(resourceRootDir, 'media', 'icons', '3d_light.svg')), @@ -82,19 +98,17 @@ export class WebviewVisu implements vscode.Disposable { case 'ready': // Webview is ready, send initialization message console.log('[WebviewVisu] Webview ready signal received'); - if (objFilenames) { + this.readyReceived = true; + if (objFilenames && objFilenames.length > 0) { console.log('[WebviewVisu] Sending init with files:', objFilenames); - const config = vscode.workspace.getConfiguration('vs-code-aster'); - const settings = { - hiddenObjectOpacity: config.get('viewer.hiddenObjectOpacity', 0), - edgeMode: config.get('viewer.edgeMode', 'threshold'), - groupTransparency: config.get('viewer.groupTransparency', 0.2), - showOrientationWidget: config.get('viewer.showOrientationWidget', true), - }; - this.panel.webview.postMessage({ - type: 'init', - body: { fileContexts, objFilenames, settings }, - }); + this.doSendInit(fileContexts, objFilenames); + } else if (this.deferredInit) { + console.log( + '[WebviewVisu] Flushing deferred init with files:', + this.deferredInit.objFilenames + ); + this.doSendInit(this.deferredInit.fileContexts, this.deferredInit.objFilenames); + this.deferredInit = undefined; } break; case 'saveSettings': @@ -141,6 +155,36 @@ export class WebviewVisu implements vscode.Disposable { console.log('[WebviewVisu] Constructor finished'); } + /** + * Send init data to the webview. If the webview has already signalled `ready`, + * posts the message immediately; otherwise buffers the data so it is sent + * when `ready` fires. + * + * Used by the standalone .med editor provider, which only has obj data + * available after running conversion asynchronously. + */ + public sendInit(fileContexts: string[], objFilenames: string[]): void { + if (this.readyReceived) { + this.doSendInit(fileContexts, objFilenames); + } else { + this.deferredInit = { fileContexts, objFilenames }; + } + } + + private doSendInit(fileContexts: string[], objFilenames: string[]): void { + const config = vscode.workspace.getConfiguration('vs-code-aster'); + const settings = { + hiddenObjectOpacity: config.get('viewer.hiddenObjectOpacity', 0), + edgeMode: config.get('viewer.edgeMode', 'threshold'), + groupTransparency: config.get('viewer.groupTransparency', 0.2), + showOrientationWidget: config.get('viewer.showOrientationWidget', true), + }; + this.panel.webview.postMessage({ + type: 'init', + body: { fileContexts, objFilenames, settings }, + }); + } + /** * Display or hide groups in the webview based on the provided text. * Groups present in the text will be displayed; others will be hidden. diff --git a/src/extension.ts b/src/extension.ts index bc61129..1800e80 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -10,6 +10,7 @@ import { RunAster } from './RunAster'; import { LspServer } from './LspServer'; import { StatusBar } from './StatusBar'; import { activateMedLanguageSync } from './MedLanguageSync'; +import { MedEditorProvider } from './MedEditorProvider'; import { setTelemetryContext } from './telemetry'; /** @@ -47,6 +48,10 @@ export async function activate(context: vscode.ExtensionContext) { activateMedLanguageSync(context); + // Register the custom editor for .med/.mmed/.rmed files so they open + // directly in the mesh viewer instead of the binary text editor. + context.subscriptions.push(MedEditorProvider.register(context)); + context.subscriptions.push(runaster); context.subscriptions.push(createExportDoc); context.subscriptions.push(createMesh); diff --git a/webviews/viewer/src/components/layout/LoadingScreen.svelte b/webviews/viewer/src/components/layout/LoadingScreen.svelte index 70c4ca8..2c0a113 100644 --- a/webviews/viewer/src/components/layout/LoadingScreen.svelte +++ b/webviews/viewer/src/components/layout/LoadingScreen.svelte @@ -1,15 +1,19 @@
-
-
-
- {$loadingMessage || 'Loading...'} + {#if $errorMessage} + {$errorMessage} + {:else} +
+
+
+ {$loadingMessage || 'Loading...'} + {/if}
diff --git a/webviews/viewer/src/lib/state.ts b/webviews/viewer/src/lib/state.ts index ce95f2b..7ad02bd 100644 --- a/webviews/viewer/src/lib/state.ts +++ b/webviews/viewer/src/lib/state.ts @@ -34,3 +34,4 @@ export const sidebarHiddenGroups = writable>>(new Map()) export const loadingProgress = tweened(0, { duration: 300, easing: cubicOut }); export const loadingMessage = writable(''); +export const errorMessage = writable(''); diff --git a/webviews/viewer/src/main.ts b/webviews/viewer/src/main.ts index b7fd10f..2306025 100644 --- a/webviews/viewer/src/main.ts +++ b/webviews/viewer/src/main.ts @@ -4,7 +4,7 @@ import { Controller } from './lib/Controller'; import { VisibilityManager } from './lib/commands/VisibilityManager'; import { CameraManager } from './lib/interaction/CameraManager'; import { GlobalSettings } from './lib/settings/GlobalSettings'; -import { settings } from './lib/state'; +import { settings, errorMessage } from './lib/state'; import type { EdgeMode } from './lib/state'; import './app.css'; @@ -61,6 +61,10 @@ window.addEventListener('message', async (e) => { case 'showOnlyObjects': VisibilityManager.Instance.showOnlyObjects(body.objects); break; + + case 'error': + errorMessage.set(body?.message || 'An error occurred.'); + break; } }); From 08da231f62c1e35d754646341177b698e63144cd Mon Sep 17 00:00:00 2001 From: Ulysse Bouchet Date: Wed, 15 Apr 2026 14:48:28 +0200 Subject: [PATCH 2/4] feat: auto-detect MED files and offer to register their extension MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Numeric MED extensions (.71, .72, …) previously required editing settings by hand. Now we peek HDF5 magic bytes when a tab opens and prompt the user with a one-click "Add .xx and open" action; the same action is exposed via an editor-title button and an explorer right-click. Once an extension is registered, newly opened tabs are auto-rerouted to the mesh viewer, which works without waiting for files.associations to propagate. --- package.json | 20 ++++ src/MedAutoDetect.ts | 226 +++++++++++++++++++++++++++++++++++++++++++ src/extension.ts | 66 ++++++++++++- 3 files changed, 311 insertions(+), 1 deletion(-) create mode 100644 src/MedAutoDetect.ts diff --git a/package.json b/package.json index 4a1ed8b..dcde24a 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,14 @@ "command": "vs-code-aster.restartLSPServer", "title": "Restart the LSP server for code_aster", "icon": "$(sync~spin)" + }, + { + "command": "vs-code-aster.addToMedExtensions", + "title": "Open as MED mesh", + "icon": { + "light": "./media/images/icone-med.svg", + "dark": "./media/images/icone-med.svg" + } } ], "languages": [ @@ -160,6 +168,18 @@ "command": "vs-code-aster.meshViewer", "when": "resourceFilename =~ /\\.(comm|com[0-9])$/", "group": "navigation" + }, + { + "command": "vs-code-aster.addToMedExtensions", + "when": "vs-code-aster.canConvertActiveToMed", + "group": "navigation" + } + ], + "explorer/context": [ + { + "command": "vs-code-aster.addToMedExtensions", + "when": "!explorerResourceIsFolder && resourceLangId != med && resourceLangId != comm && resourceLangId != export", + "group": "navigation@99" } ] }, diff --git a/src/MedAutoDetect.ts b/src/MedAutoDetect.ts new file mode 100644 index 0000000..ec40eae --- /dev/null +++ b/src/MedAutoDetect.ts @@ -0,0 +1,226 @@ +/** + * Helpers for recognizing MED mesh files without relying on extension alone. + * + * MED files are HDF5 files, and HDF5 files start with a fixed 8-byte magic + * signature. This module uses that to: + * 1. Detect when a user opens a file that looks like MED (auto-prompt). + * 2. Provide a command that adds the active file's extension to the MED + * list and re-opens the file in the mesh viewer. + * + * The prompt can be silenced per-extension by the user via the "Don't ask for + * .xx" option, which persists in `globalState`. + */ +import * as vscode from 'vscode'; +import * as path from 'path'; +import * as fs from 'fs'; +import { MedEditorProvider, STATIC_MED_EXTS } from './MedEditorProvider'; + +const HDF5_MAGIC = Buffer.from([0x89, 0x48, 0x44, 0x46, 0x0d, 0x0a, 0x1a, 0x0a]); +const DECLINED_EXTS_KEY = 'declinedMedExtensions'; + +/** + * Check if a file on disk starts with the HDF5 magic signature. + * Only reads the first 8 bytes — cheap even for huge mesh files. + */ +export async function isHdf5File(fsPath: string): Promise { + let handle: fs.promises.FileHandle | undefined; + try { + handle = await fs.promises.open(fsPath, 'r'); + const buffer = Buffer.alloc(HDF5_MAGIC.length); + const { bytesRead } = await handle.read(buffer, 0, HDF5_MAGIC.length, 0); + return bytesRead === HDF5_MAGIC.length && buffer.equals(HDF5_MAGIC); + } catch { + return false; + } finally { + await handle?.close(); + } +} + +/** + * Check if the given extension is already in `vs-code-aster.medFileExtensions` + * (case-insensitive). Exported so `extension.ts` can use the same source of + * truth when deciding whether to route a tab to the mesh viewer. + */ +export function isExtensionConfigured(ext: string): boolean { + const config = vscode.workspace.getConfiguration('vs-code-aster'); + const current = config.get('medFileExtensions', ['.med', '.mmed', '.rmed']); + const normalized = ext.toLowerCase(); + return current.some((e) => e.toLowerCase() === normalized); +} + +/** + * Append an extension to `vs-code-aster.medFileExtensions` (globally). + * No-op if already present. + */ +async function addExtensionToConfig(ext: string): Promise { + if (isExtensionConfigured(ext)) { + return; + } + const config = vscode.workspace.getConfiguration('vs-code-aster'); + const current = config.get('medFileExtensions', ['.med', '.mmed', '.rmed']); + await config.update( + 'medFileExtensions', + [...current, ext.toLowerCase()], + vscode.ConfigurationTarget.Global + ); +} + +/** + * Close any text-editor tabs currently showing the given file URI. Called + * before opening the custom editor so we don't end up with two tabs for the + * same file (the stale binary-warning text tab plus the new viewer tab). + */ +async function closeTextTabsForUri(uri: vscode.Uri): Promise { + const targetPath = uri.fsPath; + const tabsToClose: vscode.Tab[] = []; + for (const group of vscode.window.tabGroups.all) { + for (const tab of group.tabs) { + if (tab.input instanceof vscode.TabInputText && tab.input.uri.fsPath === targetPath) { + tabsToClose.push(tab); + } + } + } + if (tabsToClose.length > 0) { + await vscode.window.tabGroups.close(tabsToClose); + } +} + +/** + * Add the given URI's extension (if any) to the MED list, then reopen the + * file in the mesh viewer. Used by both the command and the auto-detect + * prompt's accept path. + */ +export async function openAsMedMesh(uri: vscode.Uri): Promise { + const ext = path.extname(uri.fsPath).toLowerCase(); + if (!ext) { + vscode.window.showWarningMessage('File has no extension to add to the MED list.'); + return; + } + await addExtensionToConfig(ext); + await closeTextTabsForUri(uri); + await vscode.commands.executeCommand('vscode.openWith', uri, MedEditorProvider.viewType); +} + +/** + * Show an info notification offering to add the file's extension to the MED + * list and reopen it in the viewer. Persistent "Don't ask" is stored per + * extension in globalState. + */ +async function promptAddMedExtension( + uri: vscode.Uri, + context: vscode.ExtensionContext +): Promise { + const ext = path.extname(uri.fsPath).toLowerCase(); + if (!ext || STATIC_MED_EXTS.has(ext) || isExtensionConfigured(ext)) { + return; + } + + const declined = context.globalState.get(DECLINED_EXTS_KEY, []); + if (declined.includes(ext)) { + return; + } + + const addLabel = `Add ${ext} and open`; + const notNow = 'Not now'; + const never = `Don't ask for ${ext}`; + const choice = await vscode.window.showInformationMessage( + `${path.basename(uri.fsPath)} looks like a MED mesh file. Add ${ext} to the MED extensions list?`, + addLabel, + notNow, + never + ); + + if (choice === addLabel) { + await openAsMedMesh(uri); + } else if (choice === never) { + await context.globalState.update(DECLINED_EXTS_KEY, [...declined, ext]); + } +} + +/** + * Context key controlling the visibility of the "Open as MED mesh" button in + * the editor title bar. Set to `true` when the active tab shows a file whose + * first bytes match the HDF5 magic and whose extension isn't already + * registered for MED. + */ +const CONTEXT_KEY = 'vs-code-aster.canConvertActiveToMed'; + +/** Extract a file URI from a Tab's input, if the tab points at a local file. */ +function getTabFileUri(tab: vscode.Tab): vscode.Uri | undefined { + const input: unknown = tab.input; + if ( + input instanceof vscode.TabInputText || + input instanceof vscode.TabInputCustom || + input instanceof vscode.TabInputNotebook + ) { + const uri = (input as { uri: vscode.Uri }).uri; + return uri.scheme === 'file' ? uri : undefined; + } + return undefined; +} + +/** + * Register listeners that peek at files as their tabs open and update the + * title-bar context key. Using the tabs API (instead of `onDidChangeActive- + * TextEditor`) is necessary because VS Code's "binary file" banner opens a + * tab without creating an active text editor — we must react to the tab + * event itself, or the auto-detect prompt never fires until the user + * manually clicks "Open anyway". + */ +export function activateMedAutoDetect(context: vscode.ExtensionContext): void { + const promptedPaths = new Set(); + + /** Check a file URI against the HDF5 magic bytes and prompt if unregistered. */ + const checkUriForPrompt = async (uri: vscode.Uri): Promise => { + const ext = path.extname(uri.fsPath).toLowerCase(); + if (!ext || STATIC_MED_EXTS.has(ext) || isExtensionConfigured(ext)) { + return false; + } + if (!(await isHdf5File(uri.fsPath))) { + return false; + } + if (!promptedPaths.has(uri.fsPath)) { + promptedPaths.add(uri.fsPath); + await promptAddMedExtension(uri, context); + } + return true; + }; + + /** Update the editor/title context key based on the currently active tab. */ + const refreshContextForActiveTab = async (): Promise => { + const activeTab = vscode.window.tabGroups.activeTabGroup?.activeTab; + let canOffer = false; + if (activeTab) { + const uri = getTabFileUri(activeTab); + if (uri) { + const ext = path.extname(uri.fsPath).toLowerCase(); + if (ext && !STATIC_MED_EXTS.has(ext) && !isExtensionConfigured(ext)) { + canOffer = await isHdf5File(uri.fsPath); + } + } + } + await vscode.commands.executeCommand('setContext', CONTEXT_KEY, canOffer); + }; + + context.subscriptions.push( + vscode.window.tabGroups.onDidChangeTabs((event) => { + for (const tab of event.opened) { + const uri = getTabFileUri(tab); + if (uri) { + void checkUriForPrompt(uri); + } + } + void refreshContextForActiveTab(); + }) + ); + + // Prime state for whatever tab is already active at activation time. + const activeTab = vscode.window.tabGroups.activeTabGroup?.activeTab; + if (activeTab) { + const uri = getTabFileUri(activeTab); + if (uri) { + void checkUriForPrompt(uri); + } + } + void refreshContextForActiveTab(); +} diff --git a/src/extension.ts b/src/extension.ts index 1800e80..760007a 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -3,6 +3,7 @@ * Handles activation, command registration, and integration with the mesh viewer and export dialog. */ import * as vscode from 'vscode'; +import * as path from 'path'; import { VisuManager } from './VisuManager'; import { ExportEditor } from './ExportEditor'; @@ -10,7 +11,8 @@ import { RunAster } from './RunAster'; import { LspServer } from './LspServer'; import { StatusBar } from './StatusBar'; import { activateMedLanguageSync } from './MedLanguageSync'; -import { MedEditorProvider } from './MedEditorProvider'; +import { MedEditorProvider, STATIC_MED_EXTS } from './MedEditorProvider'; +import { activateMedAutoDetect, isExtensionConfigured, openAsMedMesh } from './MedAutoDetect'; import { setTelemetryContext } from './telemetry'; /** @@ -52,6 +54,68 @@ export async function activate(context: vscode.ExtensionContext) { // directly in the mesh viewer instead of the binary text editor. context.subscriptions.push(MedEditorProvider.register(context)); + // Auto-detect MED files by HDF5 magic bytes, and offer to add their + // extension to the list — avoids having to edit settings manually. + activateMedAutoDetect(context); + + const addToMedExtensions = vscode.commands.registerCommand( + 'vs-code-aster.addToMedExtensions', + async (resource?: vscode.Uri) => { + const uri = + resource instanceof vscode.Uri ? resource : vscode.window.activeTextEditor?.document.uri; + if (!uri) { + vscode.window.showWarningMessage('No file to open as MED mesh.'); + return; + } + await openAsMedMesh(uri); + } + ); + context.subscriptions.push(addToMedExtensions); + + // For user-configured numeric MED extensions (e.g. .71, .72), VS Code + // opens them as text (our customEditors selectors are static). We listen + // for new tabs and, if a tab points at a file whose extension is in the + // configured MED list, close the text tab and open with the mesh viewer. + // We match against `medFileExtensions` directly (rather than + // `languageId === 'med'`) because `files.associations` updates are applied + // lazily by VS Code — relying on language ID would require a window reload. + const redirectTextTabToViewer = async (tab: vscode.Tab): Promise => { + if (!(tab.input instanceof vscode.TabInputText)) { + return; + } + const uri = tab.input.uri; + if (uri.scheme !== 'file') { + return; + } + const ext = path.extname(uri.fsPath).toLowerCase(); + if (STATIC_MED_EXTS.has(ext)) { + return; + } + if (!isExtensionConfigured(ext)) { + return; + } + + // Close the text tab first so we don't end up with two tabs for the + // same file (text + custom editor). + await vscode.window.tabGroups.close(tab); + await vscode.commands.executeCommand('vscode.openWith', uri, MedEditorProvider.viewType); + }; + + context.subscriptions.push( + vscode.window.tabGroups.onDidChangeTabs((event) => { + for (const tab of event.opened) { + void redirectTextTabToViewer(tab); + } + }) + ); + + // Handle the case where VS Code restored a text tab at startup for a file + // whose extension was added to the MED list in a previous session. + const activeTab = vscode.window.tabGroups.activeTabGroup?.activeTab; + if (activeTab) { + void redirectTextTabToViewer(activeTab); + } + context.subscriptions.push(runaster); context.subscriptions.push(createExportDoc); context.subscriptions.push(createMesh); From 335800ebf530a12cd49466b8b229d2511428fd81 Mon Sep 17 00:00:00 2001 From: Ulysse Bouchet Date: Wed, 15 Apr 2026 15:02:32 +0200 Subject: [PATCH 3/4] feat: use shared orange cube icon for mesh viewer tab --- src/WebviewVisu.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/WebviewVisu.ts b/src/WebviewVisu.ts index 5b40ab9..4322eb0 100644 --- a/src/WebviewVisu.ts +++ b/src/WebviewVisu.ts @@ -79,8 +79,8 @@ export class WebviewVisu implements vscode.Disposable { this.panel = vscode.window.createWebviewPanel(viewType, title, viewColumn, options); } this.panel.iconPath = { - light: vscode.Uri.file(path.join(resourceRootDir, 'media', 'icons', '3d.svg')), - dark: vscode.Uri.file(path.join(resourceRootDir, 'media', 'icons', '3d_light.svg')), + light: vscode.Uri.file(path.join(resourceRootDir, 'media', 'images', 'icone-med.svg')), + dark: vscode.Uri.file(path.join(resourceRootDir, 'media', 'images', 'icone-med.svg')), }; console.log('[WebviewVisu] Webview panel created'); From 5d31d48a0649d196bcd1fb5218c5e7a7313852a4 Mon Sep 17 00:00:00 2001 From: Ulysse Bouchet Date: Wed, 15 Apr 2026 15:07:13 +0200 Subject: [PATCH 4/4] [1.6.1] Bump version to 1.6.1 --- CHANGELOG.md | 12 ++++++++++++ CITATION.cff | 2 +- README.md | 2 +- ROADMAP.md | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 18 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d84ef7..12bdbf4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ 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.6.1] - 2026-04-15 + +Standalone mesh visualization: click any `.med` file to open the viewer directly, even without a `.comm`/`.export` pair. + +### Added +- Click a `.med` / `.mmed` / `.rmed` file in the explorer to open it straight in the mesh viewer, via a custom editor registered with `priority: "default"` that bypasses the "file is binary" warning +- Automatic MED detection: when a tab opens a file whose first bytes match the HDF5 signature, a notification offers to register the extension (e.g. `.71`) and open it in the viewer in one click +- "Open as MED mesh" action exposed as an editor-title button (on auto-detected MED files) and as a right-click entry in the explorer +- Tabs for files whose extensions are in `vs-code-aster.medFileExtensions` are auto-rerouted to the mesh viewer, no window reload required after registering a new extension +- The mesh viewer tab now carries the shared orange cube icon (same as `.med` files in the file tree) +- Inline error state in the viewer: when `.med`→`.obj` conversion fails (e.g. `medcoupling` not installed), the reason is shown in the tab instead of an indefinite loading screen + ## [1.6.0] - 2026-04-15 Run workflow overhaul: terminal reuse, automatic diagnostics in the Problems panel, and refreshed toolbar icons. diff --git a/CITATION.cff b/CITATION.cff index f075ea4..7950d6d 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -1,4 +1,4 @@ -cff-version: 1.6.0 +cff-version: 1.6.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 7a24027..76bf4ba 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@

Simvia Logo

- Version + Version License

diff --git a/ROADMAP.md b/ROADMAP.md index 49c3f2b..fbfaa89 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.6.0) +## Current Capabilities (v1.6.1) - `.export` file generator - 3D mesh viewer diff --git a/package-lock.json b/package-lock.json index bc4d48e..1b1a5ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "vs-code-aster", - "version": "1.6.0", + "version": "1.6.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "vs-code-aster", - "version": "1.6.0", + "version": "1.6.1", "license": "GPL-3.0", "dependencies": { "@tailwindcss/cli": "^4.1.17", diff --git a/package.json b/package.json index dcde24a..deb8c15 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vs-code-aster", "displayName": "VS Code Aster", - "version": "1.6.0", + "version": "1.6.1", "description": "VS Code extension for code_aster", "publisher": "simvia", "license": "GPL-3.0",