Skip to content

Commit 0851d86

Browse files
ww-mwclaude
andcommitted
Open Model Reference / External Data links on click; bump to 1.2.5
Clicking a Model References or External Data hyperlink in the .slx view did nothing. The click reached the host intact, but handleNavigate only understood the Usage-column grammar (name@source); these links carry a bare filename with no @source, so parseNavTarget returned null and the handler bailed. Add parseFileTarget for the bare-filename grammar and have handleNavigate try it first: resolve the file by basename in the workspace and open it (no row to select). Usage links are unchanged. Covered by navTarget unit tests and a new handleNavigate integration suite. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 46ff0f7 commit 0851d86

6 files changed

Lines changed: 116 additions & 8 deletions

File tree

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "data-explorer-vscode",
33
"displayName": "Simulink Data Explorer",
44
"description": "Explore Simulink models, data dictionaries, MAT-files, and projects as interactive tables and relationship trees.",
5-
"version": "1.2.4",
5+
"version": "1.2.5",
66
"publisher": "mathworks",
77
"icon": "media/icon.png",
88
"private": true,

src/host/navTarget.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,14 @@ export function parseNavTarget(target: string): { name: string; source: string }
2222
if (!name || !source) return null;
2323
return { name, source };
2424
}
25+
26+
// Model Reference / External Data links use a different, simpler grammar than
27+
// Usage links: their target is a bare filename (e.g. "plant.slx", "signals.mat",
28+
// "common.sldd") that just means "open this file" — there is no row inside it to
29+
// select. These carry NO '@source' suffix, so parseNavTarget rejects them.
30+
// Return the basename to open, or null when the target is a Usage-link (has an
31+
// '@') that parseNavTarget should handle instead.
32+
export function parseFileTarget(target: string): string | null {
33+
if (!target || target.includes('@')) return null;
34+
return target;
35+
}

src/host/navigate.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@
1414
// component (block->param links carry only the dictionary name); the latter is
1515
// resolved against the workspace.
1616
import * as vscode from 'vscode';
17-
import { parseNavTarget } from './navTarget.js';
17+
import { parseNavTarget, parseFileTarget } from './navTarget.js';
1818

19-
export { parseNavTarget };
19+
export { parseNavTarget, parseFileTarget };
2020

2121
// Pending selection per target uri, consumed by that editor's next paint. This
2222
// covers the just-opened case: the click fires requestSelect BEFORE the new
@@ -54,9 +54,19 @@ async function resolveSource(source: string): Promise<vscode.Uri | undefined> {
5454
return matches[0];
5555
}
5656

57-
// Handle a Usage-cell link click: open the target file (via `open`, the host's
58-
// content-aware editor router) and request selection of the referenced row.
57+
// Handle a link click. Two grammars land here:
58+
// - Usage-cell links carry `name@source`: open the source file (via `open`, the
59+
// host's content-aware editor router) and select the referenced row there.
60+
// - Model Reference / External Data links carry a bare filename: just open that
61+
// file, resolved by basename against the workspace. There is no row to select.
5962
export async function handleNavigate(target: string, open: (uri: vscode.Uri) => Promise<void>): Promise<void> {
63+
const fileTarget = parseFileTarget(target);
64+
if (fileTarget) {
65+
const uri = await resolveSource(fileTarget);
66+
if (!uri) return;
67+
await open(uri);
68+
return;
69+
}
6070
const parsed = parseNavTarget(target);
6171
if (!parsed) return;
6272
const uri = await resolveSource(parsed.source);
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// Copyright 2026 The MathWorks, Inc.
2+
// Integration tests for handleNavigate — the host-side link-click handler shared
3+
// by the binary and text editors (wired in extension.ts). Run inside a real VS
4+
// Code so vscode.workspace.findFiles resolves against the fixture workspace,
5+
// which the vitest suite cannot do (navigate.ts imports `vscode`). This covers
6+
// the Model Reference / External Data links, whose target is a BARE filename
7+
// (e.g. "data.sldd") with no "@source" suffix: the handler must look the file up
8+
// by basename and open it. The pure target grammar is unit-tested in
9+
// test/navTarget.test.ts; here we prove the end-to-end resolve-and-open.
10+
import * as assert from 'assert';
11+
import * as vscode from 'vscode';
12+
import { handleNavigate } from '../../src/host/navigate';
13+
14+
function wsUri(name: string): vscode.Uri {
15+
const ws = vscode.workspace.workspaceFolders?.[0];
16+
assert.ok(ws, 'a workspace folder must be open');
17+
return vscode.Uri.joinPath(ws.uri, name);
18+
}
19+
20+
suite('handleNavigate — Model Reference / External Data links', () => {
21+
test('a bare filename target resolves in the workspace and opens that file', async () => {
22+
const opened: vscode.Uri[] = [];
23+
// "data.sldd" is a fixture file; the link target for External Data / Model
24+
// Reference rows is exactly this bare basename (no "@source").
25+
await handleNavigate('data.sldd', async (uri) => {
26+
opened.push(uri);
27+
});
28+
assert.strictEqual(opened.length, 1, 'the target file is opened exactly once');
29+
assert.strictEqual(
30+
opened[0].toString(),
31+
wsUri('data.sldd').toString(),
32+
'the resolved Uri points at the workspace file',
33+
);
34+
});
35+
36+
test('a bare .slx model-reference target opens the referenced model', async () => {
37+
const opened: vscode.Uri[] = [];
38+
await handleNavigate('model.slx', async (uri) => {
39+
opened.push(uri);
40+
});
41+
assert.strictEqual(opened.length, 1, 'the referenced model is opened');
42+
assert.strictEqual(opened[0].toString(), wsUri('model.slx').toString());
43+
});
44+
45+
test('a bare filename with no matching workspace file opens nothing', async () => {
46+
let called = false;
47+
await handleNavigate('does-not-exist.slx', async () => {
48+
called = true;
49+
});
50+
assert.strictEqual(called, false, 'no file is opened when nothing resolves');
51+
});
52+
53+
test('a Usage-link target (name@source) is not treated as a bare file', async () => {
54+
// "Kp@data.sldd" carries an '@', so the file-target fast path must decline it
55+
// and the Usage-link path handles it: it resolves the SOURCE (data.sldd) and
56+
// opens that, not a file literally named "Kp@data.sldd".
57+
const opened: vscode.Uri[] = [];
58+
await handleNavigate('Kp@data.sldd', async (uri) => {
59+
opened.push(uri);
60+
});
61+
assert.strictEqual(opened.length, 1, 'the source file is opened');
62+
assert.strictEqual(
63+
opened[0].toString(),
64+
wsUri('data.sldd').toString(),
65+
'the Usage-link resolves its @source, not the whole target string',
66+
);
67+
});
68+
});

test/navTarget.test.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Copyright 2026 The MathWorks, Inc.
22
import { describe, it, expect } from 'vitest';
3-
import { parseNavTarget } from '../src/host/navTarget.js';
3+
import { parseNavTarget, parseFileTarget } from '../src/host/navTarget.js';
44

55
describe('parseNavTarget — Usage-link target grammar', () => {
66
it('parses a bare <name>@<basename> (block -> dictionary variable)', () => {
@@ -38,3 +38,22 @@ describe('parseNavTarget — Usage-link target grammar', () => {
3838
expect(parseNavTarget('blocks:@x')).toBeNull();
3939
});
4040
});
41+
42+
describe('parseFileTarget — Model Reference / External Data bare-file links', () => {
43+
it('accepts a bare model-reference basename (no @, no prefix)', () => {
44+
// ModelReferenceNode / DataSourceNode set linkTarget to the bare filename.
45+
expect(parseFileTarget('plant.slx')).toBe('plant.slx');
46+
expect(parseFileTarget('signals.mat')).toBe('signals.mat');
47+
expect(parseFileTarget('common.sldd')).toBe('common.sldd');
48+
});
49+
50+
it('returns null for a Usage-link target (has @source) so it does not hijack it', () => {
51+
expect(parseFileTarget('Kp@controller.sldd')).toBeNull();
52+
expect(parseFileTarget('blocks:Gain1@file:///w/plant.slx')).toBeNull();
53+
expect(parseFileTarget('workspace:Ts@model.slx')).toBeNull();
54+
});
55+
56+
it('returns null for empty input', () => {
57+
expect(parseFileTarget('')).toBeNull();
58+
});
59+
});

0 commit comments

Comments
 (0)