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
1 change: 0 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ All notable changes to the Simulink Data Explorer extension.

- You can now add variables, strings and Simulink objects to MAT-files from the Add gallery.
- Bus elements in MAT-files can be added and removed.
- Properties of Simulink objects in MAT-files can be edited in the Properties panel.
- Struct fields in MAT-files can be renamed.

## [1.43.0] — 2026-10-21
Expand Down
25 changes: 2 additions & 23 deletions src/common/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,15 +231,6 @@ export interface ValidationErrorMessage {
export interface ShowPropsMessage {
type: 'showProps';
groups: any[];
// The node the groups are of, which an edit made in the panel names (PropertyEditMessage).
nodeId?: string;
}

/** The panel's last edit was refused: why, in core's words. Follows the showProps that reverts it. */
export interface PropertyRefusedMessage {
type: 'propertyRefused';
propName: string;
reason: string;
}

/** Clear the inspector (nothing selected). */
Expand Down Expand Up @@ -360,7 +351,7 @@ export type HostToTableMessage =
| ChildRowsMessage;

/** Every message the property-inspector webview can receive from the host. */
export type HostToPropsMessage = ShowPropsMessage | EmptyMessage | MatrixCellsMessage | PropertyRefusedMessage;
export type HostToPropsMessage = ShowPropsMessage | EmptyMessage | MatrixCellsMessage;

// --- Webview -> Host (table view -> providers) --------------------------------

Expand Down Expand Up @@ -565,16 +556,4 @@ export type ProjectToHostMessage = ReadyMessage | OpenFileMessage;
// same thing from either webview, and the host answers both with the same closure. The
// same goes for `RequestMatrixMessage`: the inspector's Value row opens the same Variable
// Editor the table's does, and it is answered by the same function.
/**
* An edit committed in the Property Inspector: property `propName` (core's key) of node
* `nodeId`, the node the panel shows. Taken only where the host gave the panel an edit channel.
*/
export interface PropertyEditMessage {
type: 'editProperty';
nodeId: string;
propName: string;
newValue: string;
oldValue: string;
}

export type PropsToHostMessage = ReadyMessage | NavigateMessage | RequestMatrixMessage | PropertyEditMessage;
export type PropsToHostMessage = ReadyMessage | NavigateMessage | RequestMatrixMessage;
20 changes: 5 additions & 15 deletions src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright 2026 The MathWorks, Inc.
import * as vscode from 'vscode';
import { SectionsTreeProvider } from './host/SectionsTreeProvider.js';
import { PropertiesViewProvider, type PropertyEditHandler } from './host/PropertiesViewProvider.js';
import { PropertiesViewProvider } from './host/PropertiesViewProvider.js';
import { BinaryEditorProvider } from './host/BinaryEditorProvider.js';
import { SlddTextEditorProvider } from './host/SlddTextEditorProvider.js';
import { BinarySlddEditorProvider } from './host/BinarySlddEditorProvider.js';
Expand Down Expand Up @@ -96,18 +96,16 @@ export function activate(context: vscode.ExtensionContext): void {
// Section rows (id `section:<key>`) have no properties, so clear the PI.
// The selection the inspector was last asked to show, so a view whose tree is replaced (a
// MAT-file reverted or reloaded from disk) can have its node shown again from the new tree.
// `editor` is the edit channel of a document whose host takes edits made in the inspector (a
// MAT-file's); the inspector is read-only for every other.
let lastSelection: { uriString: string; rowIds: string[] } | undefined;
const showSelection = (uriString: string, rowIds: string[], editor?: PropertyEditHandler): void => {
const showSelection = (uriString: string, rowIds: string[]): void => {
lastSelection = { uriString, rowIds };
const first = rowIds && rowIds.length > 0 ? rowIds[0] : undefined;
if (!first || isSectionRowId(first)) {
piProvider.clear();
return;
}
const node = findNode(uriString, first);
if (node) piProvider.showNode(node, uriString, editor);
if (node) piProvider.showNode(node, uriString);
else piProvider.clear();
};

Expand Down Expand Up @@ -141,20 +139,12 @@ export function activate(context: vscode.ExtensionContext): void {
// A MAT-file opens in this writable provider (edits through core's session, save
// through core's writer). Shares the selection→PI and navigate wiring.
const matProvider = new MatEditorProvider(context);
// Its inspector takes edits: each is a gesture of the MAT-file editor's, after which the node is
// shown again (a mode edit of a NumericType derives its other properties), with the reason
// core gave where it refused.
const matInspector = (uriString: string): PropertyEditHandler => (nodeId, propName, newValue, oldValue) => {
const result = matProvider.editFromInspector(uriString, nodeId, propName, newValue, oldValue);
showSelection(uriString, [result.nodeId ?? nodeId], matInspector(uriString));
if (result.refused) piProvider.refuse(propName, result.refused);
};
matProvider.onSelect = (uriString, rowIds) => showSelection(uriString, rowIds, matInspector(uriString));
matProvider.onSelect = (uriString, rowIds) => showSelection(uriString, rowIds);
matProvider.onNavigate = navigate;
// Node ids are paths, so the node the inspector shows is found again in the new tree, or the
// inspector is cleared if it is gone; it never goes on showing a node of the tree that left.
matProvider.onReplaced = (uriString) => {
if (lastSelection?.uriString === uriString) showSelection(uriString, lastSelection.rowIds, matInspector(uriString));
if (lastSelection?.uriString === uriString) showSelection(uriString, lastSelection.rowIds);
};

// Health badges/colors on tree rows (missing/cycle/modified). The tree
Expand Down
64 changes: 0 additions & 64 deletions src/host/MatEditorProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -485,13 +485,6 @@ function backupSource(id: string): { uri: vscode.Uri; stamp: DiskStamp | undefin
* plain variable and a struct, and the classes of MATLAB's own default objects where the file's
* objects can be added to. A core without the list adds a plain variable only, as it always has.
*/
/** What an edit from the Property Inspector came to: the node's id after it, or why it was refused. */
export interface InspectorResult {
nodeId?: string;
refused?: string;
}
type InspectorEdit = (nodeId: string, propName: string, newValue: string, oldValue: string) => InspectorResult;

function allowedClasses(mat: any): readonly string[] {
if (typeof mat?.getAllowedTypes !== 'function') return ['MatlabVariable'];
const classes = mat.getAllowedTypes();
Expand All @@ -503,16 +496,6 @@ function tileLabelOf(className: string): string {
return matCatalog([className]).flatMap((c) => c.tiles).find((t) => t.className === className)?.label ?? className;
}

/** The label the inspector shows property `key` of `node` by, or the key where it shows none. */
function inspectorLabelOf(node: any, key: string): string {
try {
const prop = node?.toPIObject?.()?.propertySheet?.properties?.find((p: any) => p.name === key);
return typeof prop?.displayName === 'string' && prop.displayName !== '' ? prop.displayName : key;
} catch {
return key;
}
}

const sameList = (a: readonly string[], b: readonly string[]): boolean => a.length === b.length && a.every((x, i) => x === b[i]);

export class MatEditorProvider implements vscode.CustomEditorProvider<MatDocument> {
Expand All @@ -526,9 +509,6 @@ export class MatEditorProvider implements vscode.CustomEditorProvider<MatDocumen
// Inspector's node again from the new tree, if it is this document's).
public onReplaced?: (uriString: string) => void;

// Each open view's edit of a node from the Property Inspector, by document (editFromInspector).
private readonly inspectorEdits = new Map<string, InspectorEdit>();

private readonly _onDidChangeCustomDocument = new vscode.EventEmitter<vscode.CustomDocumentEditEvent<MatDocument>>();
readonly onDidChangeCustomDocument = this._onDidChangeCustomDocument.event;

Expand Down Expand Up @@ -745,35 +725,6 @@ export class MatEditorProvider implements vscode.CustomEditorProvider<MatDocumen
if (msg.columnId === 'Name') webview.postMessage({ type: 'selectRows', rowIds: [node.id] });
};

// An edit from the Property Inspector: the same gesture as a table edit, through core's
// editProperty with the inspector's key (wordLength, dataScope, …), its rows repainted from the
// tree; a refusal is returned for the inspector to say, the variable repainted as it was.
const applyInspectorEdit: InspectorEdit = (nodeId, propName, newValue, oldValue) => {
const node = findNode(nodeId);
if (!node) return { refused: 'Could not locate the item in the model.' };
// What the user knows the property as: the inspector's label for it ('Word Length'), not its key.
const label = inspectorLabelOf(node, propName);
let result: unknown;
const observed = gesture('Edit ' + label, (mat) => {
DataModel.setActive(mat, node);
result = DataModel.editProperty(node.id, propName, newValue, oldValue);
});
if (!observed) return { refused: `${name} cannot be edited.` };
const refused = result as { error?: boolean; reason?: string } | boolean;
if (refused && typeof refused === 'object' && refused.error) {
for (let v = node; v; v = v.parent) {
if (v.parent === document.model()) {
repaintVariables([v]);
break;
}
}
return { refused: document.describe(refused.reason) };
}
if (refused === false) return { refused: `Could not edit ${label} of this item.` };
if (propName === 'Name') webview.postMessage({ type: 'selectRows', rowIds: [node.id] });
return { nodeId: node.id };
};

// Delete the rows the gesture named, as ONE edit: the variables in one batch (one step), and
// each row under a variable on its own (one step each). Nodes are resolved first and each is
// deleted by the id it has at that moment: an element's id is its index, which the removal of
Expand Down Expand Up @@ -901,30 +852,15 @@ export class MatEditorProvider implements vscode.CustomEditorProvider<MatDocumen

webview.html = renderTableWebview(webview, distRoot);

this.inspectorEdits.set(uriString, applyInspectorEdit);

webviewPanel.onDidDispose(() => {
document.views.delete(view);
if (this.inspectorEdits.get(uriString) === applyInspectorEdit) this.inspectorEdits.delete(uriString);
unregisterAddGalleryView(webviewPanel);
sub.dispose();
navSub.dispose();
usageSub.dispose();
});
}

/**
* An edit made in the Property Inspector of node `nodeId` of the document at `uriString`: one
* gesture through core's session, as a table edit is (editProperty with the PI's key, one undo
* step, the rows repainted), whose refusal comes back as the reason, in core's words, rather
* than as a cell's validation error. Returns the node's id after the edit (a rename moves it),
* so the inspector shows it again, derived properties and all.
*/
editFromInspector(uriString: string, nodeId: string, propName: string, newValue: string, oldValue: string): InspectorResult {
const edit = this.inspectorEdits.get(uriString);
return edit ? edit(nodeId, propName, newValue, oldValue) : { refused: 'The file is no longer open for editing.' };
}

// --- Save / backup / revert --------------------------------------------------------------

async saveCustomDocument(document: MatDocument, _token: vscode.CancellationToken): Promise<void> {
Expand Down
42 changes: 6 additions & 36 deletions src/host/PropertiesViewProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,11 @@ import { matrixCellsMessage } from './matrixRequest.js';
import { renderWebviewHtml } from './webviewHtml.js';
import type { PropsToHostMessage } from '../common/protocol.js';

/**
* Where an edit made in the panel goes: the host of the node's document, which makes it a
* gesture of its own (one undo step, core's refusal reported back through `refuse`). Handed to
* showNode by a host that takes such edits; the panel is read-only for the others.
*/
export type PropertyEditHandler = (nodeId: string, propName: string, newValue: string, oldValue: string) => void;

export class PropertiesViewProvider implements vscode.WebviewViewProvider {
public static readonly viewType = 'dataExplorer.properties';
private view: vscode.WebviewView | null = null;
private ready = false;
private pending: { groups: any[]; nodeId?: string } | null = null; // pending groups
// Where an edit of the node on display goes, or null: the panel is then read-only.
private editor: PropertyEditHandler | null = null;
private pending: any[] | null = null; // pending groups
// The node currently on display, kept for `requestMatrix` only: its Value row may
// carry a MatrixDescriptor, and the cells behind one are fetched when the panel
// opens rather than stamped onto the row (see matrixRequest.ts for why).
Expand Down Expand Up @@ -46,7 +37,7 @@ export class PropertiesViewProvider implements vscode.WebviewViewProvider {
if (msg?.type === 'ready') {
this.ready = true;
if (this.pending) {
view.webview.postMessage({ type: 'showProps', ...this.pending });
view.webview.postMessage({ type: 'showProps', groups: this.pending });
this.pending = null;
}
} else if (msg?.type === 'navigate') {
Expand All @@ -55,52 +46,31 @@ export class PropertiesViewProvider implements vscode.WebviewViewProvider {
this.onNavigate?.(msg.target, this.shownFrom);
} else if (msg?.type === 'requestMatrix') {
this.answerMatrix(view.webview, msg.nodeId);
} else if (msg?.type === 'editProperty') {
// Only an edit of the node on display, and only where its host takes one: a stale
// edit (the selection moved while it was typed) or one no host takes changes nothing.
if (this.editor && this.shown && this.shown.id === msg.nodeId) {
this.editor(msg.nodeId, msg.propName, msg.newValue, msg.oldValue);
}
}
});
view.onDidDispose(() => {
this.view = null;
this.ready = false;
this.shown = null;
this.shownFrom = undefined;
this.editor = null;
});
view.webview.html = this.getHtml(view.webview, distRoot);
}

/**
* Show `node`'s properties. With `editor`, the rows core says are editable are, and an edit
* of one goes to it; without, the panel is read-only.
*/
showNode(node: any, fromUri?: string, editor?: PropertyEditHandler): void {
const groups = buildPropertyGroups(node, { editable: !!editor });
showNode(node: any, fromUri?: string): void {
const groups = buildPropertyGroups(node);
this.shown = node;
this.shownFrom = fromUri;
this.editor = editor ?? null;
const payload = { groups, ...(typeof node?.id === 'string' ? { nodeId: node.id } : {}) };
if (this.view && this.ready) {
this.view.webview.postMessage({ type: 'showProps', ...payload });
this.view.webview.postMessage({ type: 'showProps', groups });
} else {
this.pending = payload;
}
}

/** Say why the panel's last edit was refused. Sent after the showProps that put the row back. */
refuse(propName: string, reason: string): void {
if (this.view && this.ready) {
this.view.webview.postMessage({ type: 'propertyRefused', propName, reason });
this.pending = groups;
}
}

clear(): void {
this.shown = null;
this.shownFrom = undefined;
this.editor = null;
if (this.view && this.ready) {
this.view.webview.postMessage({ type: 'empty' });
} else {
Expand Down
17 changes: 2 additions & 15 deletions src/host/piBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ export interface PIPropertyRow {
name: string;
value: string;
editable: boolean;
// On an editable row: the property's key, as core's editProperty takes it ('wordLength'),
// where `name` is the label the panel shows ('Word Length').
key?: string;
type: 'text' | 'link';
linkTarget?: string;
// A link on the VALUE, set by core on the Data Type property when the type names another
Expand All @@ -27,17 +24,9 @@ export interface PIPropertyGroup {
properties: PIPropertyRow[];
}

export interface PIBuildOptions {
// Whether the panel can write an edit back: its host handed it an edit channel for this
// node's document (PropertiesViewProvider.showNode). Then a row is editable exactly where core
// says it is (toPIObject's `editable`), and carries the key core's editProperty takes.
// Without one every row is read-only, as the dictionary and model views' panels are.
editable?: boolean;
}

// Replicates the vendored two-step transform (PIController.convertToPIObject +
// LitRenderer.setPIContent). graphture/usedBy extras are intentionally omitted.
export function buildPropertyGroups(node: any, options: PIBuildOptions = {}): PIPropertyGroup[] {
export function buildPropertyGroups(node: any): PIPropertyGroup[] {
if (!node || typeof node.toPIObject !== 'function') return [];
const raw = node.toPIObject();
if (!raw || !raw.propertySheet) return [];
Expand All @@ -56,12 +45,10 @@ export function buildPropertyGroups(node: any, options: PIBuildOptions = {}): PI
// folded into `link`: that one would make this a `type: 'link'` row, whose template
// anchors the property name and mutes the value.
const valueLink = (propDef as any).valueLink;
const editable = !!options.editable && propDef.editable === true && !link;
const row: PIPropertyRow = {
name: propDef.displayName || propDef.name,
value: String(obj[propDef.name] ?? ''),
editable,
...(editable ? { key: propDef.name } : {}),
editable: false, // read-only V1
type: link ? 'link' : 'text',
linkTarget: link || undefined,
...(typeof valueLink === 'string' && valueLink !== '' ? { valueLink } : {}),
Expand Down
17 changes: 1 addition & 16 deletions src/webview/components/dex-property-inspector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ export interface PropertyRow {
name: string;
value: string;
editable?: boolean;
// The key an edit of an editable row names (piBuilder): core's, where `name` is its label.
key?: string;
type?: 'text' | 'link';
linkTarget?: string;
// A link on the VALUE (piBuilder, from core's toPIObject): the Data Type of an entry
Expand Down Expand Up @@ -135,22 +133,10 @@ export class DexPropertyInspector extends LitElement {
font-size: 11px;
color: var(--dex-color-text-secondary, #888);
}

.prop-refusal {
margin: 4px 8px;
padding: 4px 6px;
font-size: 12px;
border-left: 2px solid var(--vscode-inputValidation-errorBorder, #be1100);
background: var(--vscode-inputValidation-errorBackground, transparent);
color: var(--vscode-errorForeground, inherit);
}
`;

@property({ type: Array }) groups: PropertyGroup[] = [];

// Why the last edit made here was refused, in core's words, until the next node is shown.
@property({ attribute: false }) refusal: string | null = null;

private _onPropertyChange(propName: string, newValue: string, oldValue: string): void {
this.dispatchEvent(new CustomEvent('dex-property-changed', {
detail: { propName, newValue, oldValue },
Expand All @@ -176,7 +162,7 @@ export class DexPropertyInspector extends LitElement {
return html`<input
class="prop-input"
.value=${live(prop.value)}
@change=${(e: Event) => this._onPropertyChange(prop.key ?? prop.name, (e.target as HTMLInputElement).value, prop.value)}
@change=${(e: Event) => this._onPropertyChange(prop.name, (e.target as HTMLInputElement).value, prop.value)}
/>`;
}
// The type this value names, as a link to its definition — the same jump the table's
Expand Down Expand Up @@ -206,7 +192,6 @@ export class DexPropertyInspector extends LitElement {
override render() {
return html`
<div class="content">
${this.refusal ? html`<div class="prop-refusal" role="alert">${this.refusal}</div>` : ''}
${this.groups.map(group => html`
<div class="group">
<div class="group-header">${group.title}</div>
Expand Down
Loading
Loading