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
22 changes: 21 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,12 @@
"title": "View as Table",
"category": "Data Explorer",
"icon": "$(table)"
},
{
"command": "dataExplorer.searchDataSources",
"title": "Search Data Source Entries",
"category": "Data Explorer",
"icon": "$(search)"
}
],
"menus": {
Expand All @@ -152,8 +158,22 @@
"command": "dataExplorer.viewAsTable",
"when": "resourceExtname == .sldd"
}
],
"view/title": [
{
"command": "dataExplorer.searchDataSources",
"when": "view == dataExplorer.sections",
"group": "navigation@1"
}
]
}
},
"keybindings": [
{
"command": "dataExplorer.searchDataSources",
"key": "ctrl+alt+e",
"mac": "cmd+alt+e"
}
]
},
"scripts": {
"build:webview": "vite build",
Expand Down
36 changes: 30 additions & 6 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import { BinarySlddEditorProvider } from './host/BinarySlddEditorProvider.js';
import { HealthDecorationProvider } from './host/HealthDecorationProvider.js';
import { invalidate, findNode } from './host/SlddModel.js';
import { isEditableJsonSlddBytes, exceedsTextSyncLimit, isZipBytes } from './host/slddFormat.js';
import { handleNavigate } from './host/navigate.js';
import { handleNavigate, requestSelect } from './host/navigate.js';
import { invalidateUsageGraph } from './host/usageGraph.js';
import { searchDataSources } from './host/searchSources.js';
import { listEntries, reindexFile, removeFile } from './host/nameIndex.js';
import { isSectionRowId } from './common/sectionRowId.js';

const SUPPORTED_RE = /\.(sldd|mat|slx|prj)$/;
Expand Down Expand Up @@ -158,13 +160,24 @@ export function activate(context: vscode.ExtensionContext): void {
piProvider,
),
watcher,
// Files added/removed change the root list.
watcher.onDidCreate(refreshAll),
watcher.onDidDelete(refreshAll),
// Files added/removed change the root list. Also keep the name index in sync:
// reindex the new file / drop the removed file's bucket. Both index ops are
// no-ops until the index is first built (by the first search), so they're
// cheap when search has never been opened.
watcher.onDidCreate((uri) => {
void reindexFile(uri);
refreshAll();
}),
watcher.onDidDelete((uri) => {
removeFile(uri.toString());
refreshAll();
}),
// A file's contents changed: drop its cached model (table) and rebuild the
// reference index (tree), since edits may add or remove references.
// reference index (tree), since edits may add or remove references. Also
// reindex its entry names (no-op until the index is first built).
watcher.onDidChange((uri) => {
invalidate(uri.toString());
void reindexFile(uri);
refreshAll();
}),
// Live edits in an open editor: invalidate the cached model and refresh.
Expand All @@ -173,8 +186,11 @@ export function activate(context: vscode.ExtensionContext): void {
invalidate(e.document.uri.toString());
}
// A dirty-state transition on any supported file changes the "modified"
// health badge, so refresh decorations for supported docs.
// health badge, so refresh decorations for supported docs. Also re-sync
// the name index for live entry-name edits (e.g. renaming an entry in an
// open .sldd); reindexFile is a no-op until the index is first built.
if (SUPPORTED_RE.test(e.document.uri.path)) {
void reindexFile(e.document.uri);
refreshAll();
}
}),
Expand Down Expand Up @@ -222,6 +238,14 @@ export function activate(context: vscode.ExtensionContext): void {
/* ignore */
}
}),
// Global entry-name search overlay: pick an entry by name across all data
// sources, then open its source file and select the matching row.
vscode.commands.registerCommand('dataExplorer.searchDataSources', () =>
searchDataSources(listEntries, async (sourceUri, entryName) => {
requestSelect(sourceUri, entryName);
await openInBestEditor(vscode.Uri.parse(sourceUri), { preview: true });
}),
),
);
}

Expand Down
22 changes: 6 additions & 16 deletions src/host/BinaryEditorProvider.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Copyright 2026 The MathWorks, Inc.
import * as vscode from 'vscode';
import { renderWebviewHtml } from './webviewHtml.js';
import { renderWebviewHtml, LOADING_OVERLAY_HTML } from './webviewHtml.js';
import { getModelFromBytes, getProjectModel, invalidate } from './SlddModel.js';
import {
buildRows,
Expand All @@ -14,7 +14,7 @@ import { buildMatRows } from './matRowBuilder.js';
import { readProjectStore } from './projectStore.js';
import { isEditableJsonSlddBytes, exceedsTextSyncLimit, exceedsStringDecodeLimit, isZipBytes } from './slddFormat.js';
import { annotateDataRows, annotateModelRows } from './usageGraph.js';
import { wireNavigateSelect, consumePendingSelect } from './navigate.js';
import { wireNavigateSelect, drainNavigateSelect } from './navigate.js';
import { basename } from '../common/pathUtil.js';
import { toArrayBuffer } from '../common/bytes.js';
import type { TableToHostMessage } from '../common/protocol.js';
Expand Down Expand Up @@ -157,12 +157,6 @@ export class BinaryEditorProvider implements vscode.CustomReadonlyEditorProvider
return toArrayBuffer(await vscode.workspace.fs.readFile(document.uri));
};

// If a cross-tab navigation targeted this file (e.g. it was just opened by a
// Usage-link click), select the requested row now that rows exist.
const drainNavSelect = (): void => {
const navName = consumePendingSelect(uriString);
if (navName) webview.postMessage({ type: 'selectByName', name: navName });
};

// Read/parse the file host-side and push rows to the webview. On failure,
// drop the cached model and post a banner.
Expand All @@ -182,7 +176,7 @@ export class BinaryEditorProvider implements vscode.CustomReadonlyEditorProvider
columnLabels: PROJECT_COLUMN_LABELS,
editable: false,
});
drainNavSelect();
drainNavigateSelect(webview, uriString);
return;
}

Expand Down Expand Up @@ -211,7 +205,7 @@ export class BinaryEditorProvider implements vscode.CustomReadonlyEditorProvider
editable: false,
notice,
});
drainNavSelect();
drainNavigateSelect(webview, uriString);
} catch (err) {
invalidate(uriString);
webview.postMessage({
Expand Down Expand Up @@ -287,14 +281,10 @@ export class BinaryEditorProvider implements vscode.CustomReadonlyEditorProvider
return renderWebviewHtml(webview, distRoot, {
scriptFile: 'table.js',
title: 'Data Explorer',
body: ` <style>@keyframes dex-spin { to { transform: rotate(360deg); } }</style>
<div id="dex-error" role="alert" style="display:none;color:var(--vscode-errorForeground,#f14c4c);padding:8px;font-family:var(--vscode-font-family,sans-serif);"></div>
body: ` <div id="dex-error" role="alert" style="display:none;color:var(--vscode-errorForeground,#f14c4c);padding:8px;font-family:var(--vscode-font-family,sans-serif);"></div>
<div id="dex-notice" role="status" style="display:none;position:absolute;top:0;left:0;right:0;z-index:2;box-sizing:border-box;padding:6px 10px;font-family:var(--vscode-font-family,sans-serif);font-size:12px;color:var(--vscode-inputValidation-infoForeground,var(--vscode-foreground));background:var(--vscode-inputValidation-infoBackground,rgba(100,148,237,0.12));border-bottom:1px solid var(--vscode-inputValidation-infoBorder,#4084d0);"></div>
<dex-tree-table style="position:absolute;inset:0;"></dex-tree-table>
<div id="dex-loading" role="status" aria-label="Loading" style="position:absolute;inset:0;z-index:3;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;font-family:var(--vscode-font-family,sans-serif);font-size:12px;color:var(--vscode-descriptionForeground,var(--vscode-foreground));background:var(--vscode-editor-background,transparent);">
<div style="width:28px;height:28px;border:3px solid var(--vscode-progressBar-background,#0e70c0);border-top-color:transparent;border-radius:50%;animation:dex-spin 0.8s linear infinite;"></div>
<div>Loading…</div>
</div>
${LOADING_OVERLAY_HTML}
<dex-context-menu></dex-context-menu>`,
});
}
Expand Down
10 changes: 9 additions & 1 deletion src/host/BinarySlddEditorProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
// cached model of the same file.
import * as vscode from 'vscode';
import { unzipSync, zipSync } from 'fflate';
import { renderWebviewHtml } from './webviewHtml.js';
import { renderWebviewHtml, LOADING_OVERLAY_HTML } from './webviewHtml.js';
import { buildRows, COLUMNS, COLUMN_LABELS, COLUMN_GROUPS, type ClipMark } from './rowBuilder.js';
import { sectionRules } from './sectionRules.js';
import { parseBinarySlddParts } from '../dex/datamodel/parser/BinarySlddParser.js';
Expand Down Expand Up @@ -49,6 +49,7 @@ import {
deleteFromSource,
} from './editorHub.js';
import { basename } from '../common/pathUtil.js';
import { wireNavigateSelect, drainNavigateSelect } from './navigate.js';
import type { TableToHostMessage } from '../common/protocol.js';

// srcId prefix so the editable model never collides with the read-only
Expand Down Expand Up @@ -185,6 +186,7 @@ export class BinarySlddEditorProvider implements vscode.CustomEditorProvider<Bin
});
webview.postMessage({ type: 'sectionRules', docUri: uriString, rules: sectionRules(node) });
webview.postMessage({ type: 'clipboardState', ...clipboardState() });
drainNavigateSelect(webview, uriString);
} catch (err) {
webview.postMessage({ type: 'error', message: `Failed to parse ${name}: ${(err as Error).message}` });
}
Expand Down Expand Up @@ -475,11 +477,16 @@ export class BinarySlddEditorProvider implements vscode.CustomEditorProvider<Bin
} else if (msg?.type === 'undo' || msg?.type === 'redo') void vscode.commands.executeCommand(msg.type);
});

// Live cross-tab selection: if a navigation targets THIS already-open file,
// select the row immediately (the just-opened case is drained in post()).
const navSub = wireNavigateSelect(webview, uriString);

webview.html = renderWebviewHtml(webview, distRoot, {
scriptFile: 'table.js',
title: 'Data Explorer',
body: ` <div id="dex-error" role="alert" style="display:none;color:var(--vscode-errorForeground,#f14c4c);padding:8px;font-family:var(--vscode-font-family,sans-serif);"></div>
<dex-tree-table style="position:absolute;inset:0;"></dex-tree-table>
${LOADING_OVERLAY_HTML}
<dex-context-menu></dex-context-menu>
<dex-error-dialog></dex-error-dialog>`,
});
Expand All @@ -494,6 +501,7 @@ export class BinarySlddEditorProvider implements vscode.CustomEditorProvider<Bin
broadcastDragState();
}
sub.dispose();
navSub.dispose();
document._afterMutate = undefined;
clearBaseline(uriString);
});
Expand Down
10 changes: 4 additions & 6 deletions src/host/SlddTextEditorProvider.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Copyright 2026 The MathWorks, Inc.
import * as vscode from 'vscode';
import { renderWebviewHtml } from './webviewHtml.js';
import { renderWebviewHtml, LOADING_OVERLAY_HTML } from './webviewHtml.js';
import { getModel, invalidate, findNode } from './SlddModel.js';
import { findEntrySpan, detectIndent } from './entrySplice.js';
import { buildRows, COLUMNS, COLUMN_LABELS, COLUMN_GROUPS, type ClipMark } from './rowBuilder.js';
Expand Down Expand Up @@ -30,7 +30,7 @@ import {
type StructuralResult,
} from './structuralEdit.js';
import { annotateDataRows } from './usageGraph.js';
import { wireNavigateSelect, consumePendingSelect } from './navigate.js';
import { wireNavigateSelect, drainNavigateSelect } from './navigate.js';
import { basename } from '../common/pathUtil.js';
import type { TableToHostMessage } from '../common/protocol.js';

Expand Down Expand Up @@ -127,10 +127,7 @@ export class SlddTextEditorProvider implements vscode.CustomTextEditorProvider {
// a drop (dropDecision) live on dragover without a host round-trip.
webview.postMessage({ type: 'sectionRules', docUri: uriString, rules: sectionRules(node) });
webview.postMessage({ type: 'clipboardState', ...clipboardState() });
// If a cross-tab navigation targeted this file (e.g. it was just
// opened by a Usage-link click), select the requested row now.
const navName = consumePendingSelect(uriString);
if (navName) webview.postMessage({ type: 'selectByName', name: navName });
drainNavigateSelect(webview, uriString);
});
} catch (err) {
invalidate(uriString);
Expand Down Expand Up @@ -661,6 +658,7 @@ export class SlddTextEditorProvider implements vscode.CustomTextEditorProvider {
title: 'Data Explorer',
body: ` <div id="dex-error" role="alert" style="display:none;color:var(--vscode-errorForeground,#f14c4c);padding:8px;font-family:var(--vscode-font-family,sans-serif);"></div>
<dex-tree-table style="position:absolute;inset:0;"></dex-tree-table>
${LOADING_OVERLAY_HTML}
<dex-context-menu></dex-context-menu>
<dex-error-dialog></dex-error-dialog>`,
});
Expand Down
80 changes: 80 additions & 0 deletions src/host/nameExtract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Copyright 2026 The MathWorks, Inc.
// Pure (vscode-free) core of the workspace name index: turns already-parsed
// Simulink data-source content into flat, dup-preserving name records. Split
// from nameIndex.ts (which does the file I/O + parser dispatch) so the
// name-extraction rules are unit-testable without touching the filesystem.
//
// This module is deliberately independent of the usage graph (usageResolve.ts)
// and the relationship graph: it answers only "what entry names exist, and
// where", never how they resolve or relate. Duplicate names across files are
// preserved (each becomes its own record) so a global "search entries by name"
// can list every occurrence.
import { uriBasename } from '../common/pathUtil.js';

export type EntryKind = 'sldd' | 'mat' | 'workspace' | 'block';

export interface NameRecord {
name: string;
sourceUri: string;
sourceLabel: string;
kind: EntryKind;
}

// Entry names from an .sldd (JSON or binary/zip; both share the in-memory
// __MW_TEXT_PARTS__ shape). Traversal mirrors usageGraph's slddSummary:
// content.__MW_TEXT_PARTS__['__MW_TEXT_PART__/data/chunk0'].__MW_TEXT_content.entries[].name.
export function namesFromSldd(content: Record<string, unknown>, sourceUri: string): NameRecord[] {
const label = uriBasename(sourceUri);
const parts = content?.__MW_TEXT_PARTS__ as Record<string, unknown> | undefined;
const chunk = parts?.['__MW_TEXT_PART__/data/chunk0'] as Record<string, unknown> | undefined;
const inner = chunk?.__MW_TEXT_content as Record<string, unknown> | undefined;
const entries = (inner?.entries as { name?: string }[] | undefined) ?? [];
const records: NameRecord[] = [];
for (const entry of entries) {
const name = entry?.name;
if (!name) continue; // drop empty/falsy names
records.push({ name, sourceUri, sourceLabel: label, kind: 'sldd' });
}
return records;
}

// Variable names from a parsed .mat.
export function namesFromMat(parsed: { variables: { name?: string }[] }, sourceUri: string): NameRecord[] {
const label = uriBasename(sourceUri);
const records: NameRecord[] = [];
for (const v of parsed?.variables ?? []) {
const name = v?.name;
if (!name) continue; // drop empty/falsy names
records.push({ name, sourceUri, sourceLabel: label, kind: 'mat' });
}
return records;
}

// Model-workspace variable names (kind 'workspace') plus referenced block names
// (kind 'block') from a parsed .slx — both live in the same model file. A block
// is emitted once even if it uses multiple params, deduped WITHIN this file via
// a Set (the usage of a block many times is a graph concern, not a name one).
export function namesFromSlx(
parsed: { workspace?: { name?: string }[]; blockParamUsages?: { blockName?: string }[] },
sourceUri: string,
): NameRecord[] {
const label = uriBasename(sourceUri);
const records: NameRecord[] = [];

for (const v of parsed?.workspace ?? []) {
const name = v?.name;
if (!name) continue; // drop empty/falsy names
records.push({ name, sourceUri, sourceLabel: label, kind: 'workspace' });
}

const seenBlocks = new Set<string>();
for (const u of parsed?.blockParamUsages ?? []) {
const name = u?.blockName;
if (!name) continue; // drop empty/falsy names
if (seenBlocks.has(name)) continue; // one record per block within this file
seenBlocks.add(name);
records.push({ name, sourceUri, sourceLabel: label, kind: 'block' });
}

return records;
}
Loading