Skip to content

Commit 9704d53

Browse files
authored
Release v0.12.15 (#101)
1 parent be1a22d commit 9704d53

14 files changed

Lines changed: 754 additions & 89 deletions

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: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"type": "git",
1414
"url": "https://github.com/wurstscript/wurst4vscode.git"
1515
},
16-
"version": "0.12.14",
16+
"version": "0.12.15",
1717
"publisher": "peterzeller",
1818
"engines": {
1919
"vscode": "^1.109.0"
@@ -491,7 +491,7 @@
491491
},
492492
{
493493
"viewType": "wurst.wpmPreview",
494-
"displayName": "WC3 Pathing Map",
494+
"displayName": "WC3 Pathing Map (editable)",
495495
"selector": [
496496
{
497497
"filenamePattern": "*.wpm"
@@ -891,6 +891,7 @@
891891
"test:webview": "node ./scripts/test-webview.js",
892892
"test:e2e:models:local": "node ./scripts/model-thumbnail-e2e.js",
893893
"test:e2e:objmod-thumbs:local": "node ./scripts/objmod-thumbnail-e2e.js",
894+
"test:e2e:asset-browser-code:local": "node ./scripts/objmod-thumbnail-e2e.js --code-only",
894895
"test:e2e:objmod-clipboard:local": "node ./scripts/objmod-clipboard-e2e.js",
895896
"test:wc3-previews": "node ./scripts/wc3-preview-smoke.js",
896897
"test:fuzzy": "node ./scripts/test-fuzzy.js",

scripts/objmod-thumbnail-e2e.js

Lines changed: 205 additions & 10 deletions
Large diffs are not rendered by default.

scripts/test-fuzzy.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,16 @@ const { fuzzyMatch, assetSearchScore } = mod.exports;
2020
assert.strictEqual(typeof fuzzyMatch, 'function', 'fuzzyMatch should be exported');
2121
assert.strictEqual(typeof assetSearchScore, 'function', 'assetSearchScore should be exported');
2222

23+
// The code-launched asset picker serializes both functions into an isolated webview. This must not
24+
// leave the scorer reaching back into its original CommonJS/webpack module closure.
25+
const isolatedFuzzyMatch = new Function(`return (${fuzzyMatch.toString()});`)();
26+
const isolatedAssetSearchScore = new Function(`return (${assetSearchScore.toString()});`)();
27+
assert.equal(
28+
isolatedAssetSearchScore('footman', 'Footman.mdx', 'units\\human\\Footman.mdx', '', isolatedFuzzyMatch),
29+
0,
30+
'serialized asset scorer should run with only its explicit fuzzy matcher dependency',
31+
);
32+
2333
let passed = 0;
2434
function ok(query, text, expected, msg) {
2535
const got = fuzzyMatch(query, text);
@@ -67,7 +77,7 @@ const footmanCandidates = [
6777
{ label: 'AltarOfKings - altarofkings', value: 'buildings\\human\\AltarOfKings\\AltarOfKings.mdx' },
6878
];
6979
const footmanResults = footmanCandidates
70-
.map((item, index) => ({ ...item, index, score: assetSearchScore('footman', item.label, item.value) }))
80+
.map((item, index) => ({ ...item, index, score: assetSearchScore('footman', item.label, item.value, '', fuzzyMatch) }))
7181
.filter((item) => Number.isFinite(item.score))
7282
.sort((a, b) => a.score - b.score || a.index - b.index);
7383
assert.deepStrictEqual(
@@ -80,6 +90,6 @@ assert.deepStrictEqual(
8090
[0, 10, 20],
8191
'asset relevance scores should be deterministic',
8292
);
83-
passed += 2;
93+
passed += 3;
8494

8595
console.log(`fuzzy unit tests passed (${passed} assertions)`);

scripts/test-webview.js

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -222,22 +222,27 @@ function installObjModStateDom(persistedState) {
222222
const els = { tree: new FakeElement(), details: new FakeElement(), search: new FakeElement() };
223223
global.document = { getElementById: (id) => els[id] || null };
224224
let state = persistedState || {};
225+
const messages = [];
225226
global.acquireVsCodeApi = () => ({
226-
postMessage: () => {},
227+
postMessage: (message) => { messages.push(message); },
227228
getState: () => state,
228229
setState: (next) => { state = next; },
229230
});
230-
return { getState: () => state };
231+
return { getState: () => state, getMessages: () => messages };
231232
}
232233

233234
// state.ts is meant to make a reopened editor (a webview reload after our external-change auto-reload
234235
// or revert, or a fresh VS Code session) pick back up where the user left off, instead of resetting to
235236
// a blank slate — see the persistUi effect and the restoredSelectedKey logic there.
236237
function testObjModStateRestoresAndPersistsUiState() {
237238
moduleCache.clear();
238-
const objects = [{ key: 'Custom:0' }, { key: 'Custom:1' }];
239+
const objects = [
240+
{ key: 'Custom:0', identity: 'Custom:hfoo|h001' },
241+
{ key: 'Custom:1', identity: 'Custom:hrif|h002' },
242+
];
239243
const dom = installObjModStateDom({
240-
selectedKey: 'Custom:1',
244+
selectedKey: 'Custom:0', // deliberately stale after an external reorder
245+
selectedIdentity: 'Custom:hrif|h002',
241246
query: 'foo',
242247
fieldQuery: 'dmg',
243248
showTechnical: true,
@@ -270,13 +275,19 @@ function testObjModStateRestoresAndPersistsUiState() {
270275
assert.equal(persistedAfter.treeScrollTop, 240, 'persisting one field must not drop the others');
271276
assert.equal(persistedAfter.detailsScrollTop, 150, 'persisting one field must not drop the others');
272277
assert.equal(persistedAfter.selectedKey, 'Custom:1', 'unrelated restored fields must survive a later persist');
278+
assert.equal(persistedAfter.selectedIdentity, 'Custom:hrif|h002', 'selection persistence must use stable rawcodes, not an array index');
279+
assert.ok(dom.getMessages().some(message => message.type === 'selectionChanged' && message.identity === 'Custom:hrif|h002'),
280+
'the stable selection identity must be sent to the host for workspace-relative persistence');
273281
assert.equal(persistedAfter.listW, 321, 'fields unrelated to reactive ui state (e.g. splitter width) must not be clobbered');
274282
}
275283

276284
function testObjModStatePendingJumpOverridesRestoredSelection() {
277285
moduleCache.clear();
278-
const objects = [{ key: 'Custom:0' }, { key: 'Custom:1' }];
279-
installObjModStateDom({ selectedKey: 'Custom:1' });
286+
const objects = [
287+
{ key: 'Custom:0', identity: 'Custom:hfoo|h001' },
288+
{ key: 'Custom:1', identity: 'Custom:hrif|h002' },
289+
];
290+
installObjModStateDom({ selectedKey: 'Custom:1', selectedIdentity: 'Custom:hrif|h002' });
280291
global.window.__OBJMOD_INITIAL__ = { objects, selectedKey: 'Custom:0', isPendingJump: true, extended: false };
281292

282293
const state = loadTsModule('src/webview/objModEditor/state.ts');
@@ -285,8 +296,8 @@ function testObjModStatePendingJumpOverridesRestoredSelection() {
285296

286297
function testObjModStateIgnoresStaleRestoredSelection() {
287298
moduleCache.clear();
288-
const objects = [{ key: 'Custom:0' }]; // 'Custom:99' below no longer exists in this file
289-
installObjModStateDom({ selectedKey: 'Custom:99' });
299+
const objects = [{ key: 'Custom:0', identity: 'Custom:hfoo|h001' }];
300+
installObjModStateDom({ selectedKey: 'Custom:99', selectedIdentity: 'Custom:old0|old1' });
290301
global.window.__OBJMOD_INITIAL__ = { objects, selectedKey: 'Custom:0', isPendingJump: false, extended: false };
291302

292303
const state = loadTsModule('src/webview/objModEditor/state.ts');
@@ -300,8 +311,8 @@ function testObjModStateIgnoresStaleRestoredSelection() {
300311
function testObjModTreeRenderPreservesScrollPosition() {
301312
moduleCache.clear();
302313
const objects = [
303-
{ key: 'Custom:0', group: 'Custom', race: 'human', displayName: 'Alpha', baseId: 'a000' },
304-
{ key: 'Custom:1', group: 'Custom', race: 'human', displayName: 'Beta', baseId: 'b000' },
314+
{ key: 'Custom:0', identity: 'Custom:a000|a001', group: 'Custom', race: 'human', displayName: 'Alpha', baseId: 'a000' },
315+
{ key: 'Custom:1', identity: 'Custom:b000|b001', group: 'Custom', race: 'human', displayName: 'Beta', baseId: 'b000' },
305316
];
306317
installObjModStateDom({ treeScrollTop: 240 });
307318
global.window.__OBJMOD_INITIAL__ = { objects, selectedKey: '', isPendingJump: false, extended: false };
@@ -628,7 +639,7 @@ function testNonBlockingStartupAndForcedReinstallWiring() {
628639
assert.ok(!extension.includes('ensureInstalledOrOfferMigration(true)'), 'manual install/update must not use the no-op ensure path');
629640
assert.ok(!languageServer.includes('await maybeOfferUpdate(context)'), 'update checks must not delay language-client startup');
630641
assert.ok(languageServer.includes('void maybeOfferUpdate((update) =>'), 'update checks should still run in the background and update the status item');
631-
assert.ok(languageServer.includes("'$(cloud-download) WurstScript Update'"), 'the status item must indicate when an update is available');
642+
assert.ok(languageServer.includes("'$(circle-filled) WurstScript Update'"), 'the status item must indicate when an update is available');
632643
assert.ok(!installer.includes("{ modal: true, detail }, 'Update', 'Later'"), 'the automatic update notification must not be modal');
633644
assert.ok(installer.includes("'Update', 'Later'"), 'the non-modal update notification must retain its actions');
634645
assert.ok(installer.includes("execFile(java, ['-jar', COMPILER_JAR, '--version']"), 'version detection must use an asynchronous child process');
@@ -752,7 +763,7 @@ function testAssetBrowserForwardsModelTextures() {
752763
'asset browser must not silently drop model texture requests'
753764
);
754765
assert.ok(
755-
script.includes('assetSearchScore(query, item.label, item.value, item.detail)'),
766+
script.includes('assetSearchScore(query, item.label, item.value, item.detail, fuzzyMatch)'),
756767
'code and object-data asset pickers should share the relevance scorer'
757768
);
758769
assert.ok(
@@ -794,7 +805,7 @@ function testThumbnailLifecycleGuards() {
794805
assert.ok(objmod.includes('fetch(initial.thumbnailWorkerUri'), 'the worker bundle must be fetched before creating its Blob URL');
795806
assert.ok(!objmod.includes('new Worker(initial.thumbnailWorkerUri)'), 'VS Code resource URLs cannot be passed directly to the Worker constructor');
796807
assert.ok(assetBrowser.includes('modelThumbEnsureInit()'), 'opening or selecting the model asset browser should prewarm the thumbnail worker');
797-
assert.ok(assetBrowser.includes("import { assetSearchScore } from '../../features/preview/fuzzy'"), 'objmod asset search should use the shared relevance scorer');
808+
assert.ok(assetBrowser.includes("import { assetSearchScore, fuzzyMatch } from '../../features/preview/fuzzy'"), 'objmod asset search should use the shared relevance scorer');
798809
const ensureInit = /export function modelThumbEnsureInit\(\) \{([\s\S]*?)\n\}/.exec(objmod)?.[1] || '';
799810
assert.ok(!ensureInit.includes('mpvViewer()'), 'worker startup failure must not fall back to rendering on the objmod UI thread');
800811
assert.ok(webpack.includes("mdxThumbnailWorker: './src/webview/mdxThumbnailWorker.ts'"), 'the isolated thumbnail worker must be bundled');
@@ -1017,6 +1028,27 @@ function testObjModEditorTypeAndRecoveryGuards() {
10171028
assert.ok(host.includes('wtsEdits: Array.from(doc.wtsEdits)'), 'objmod backups must include staged WTS edits');
10181029
assert.ok(host.includes('currentRevision = beforeRevision'), 'undo must restore a history identity, not decrement a depth');
10191030
assert.ok(!host.includes('doc.editDepth'), 'branch-unsafe edit depth tracking must not return');
1031+
assert.ok(host.includes('watcher.onDidDelete(onEvent)'), 'external-change detection must cover Git-style file replacement');
1032+
assert.ok(host.includes('id="refresh-editor"'), 'the object editor must expose a manual refresh action');
1033+
assert.ok(host.includes('preferredSelectionIdentity'), 'reloading must resolve selection by stable object identity');
1034+
assert.ok(host.includes('objModSelectionPathKey(doc.uri)'), 'selection must be stored per workspace-relative document path');
1035+
}
1036+
1037+
function testWpmEditorInlineScriptAndRecoveryGuards() {
1038+
const host = fs.readFileSync(path.join(root, 'src/features/wpmPreview.ts'), 'utf8');
1039+
const match = host.match(/<script>\r?\n([\s\S]*?)\r?\n {2}<\/script>/);
1040+
assert.ok(match, 'WPM editor inline script should be present');
1041+
const script = match[1]
1042+
.replace('${wpm.width}', '4')
1043+
.replace('${wpm.height}', '4')
1044+
.replace('${dataBase64}', 'AAAAAAAAAAAAAAAAAAAAAA==')
1045+
.replace(/\\`/g, '`')
1046+
.replace(/\\\$\{/g, '${');
1047+
// eslint-disable-next-line sonarjs/constructor-for-side-effects -- parsing the real inline script is the assertion.
1048+
new vm.Script(script);
1049+
assert.ok(host.includes('openContext.backupId'), 'WPM documents must restore VS Code hot-exit backups');
1050+
assert.ok(host.includes('currentRevision !== doc.savedRevision'), 'WPM dirty tracking must distinguish edit-history branches');
1051+
assert.ok(!host.includes('doc.editDepth'), 'WPM dirty tracking must not use branch-unsafe edit depth');
10201052
}
10211053

10221054
async function main() {
@@ -1033,6 +1065,7 @@ async function main() {
10331065
testBc5DdsDecode();
10341066
testInstallerVersionShaParsing();
10351067
testObjModEditorTypeAndRecoveryGuards();
1068+
testWpmEditorInlineScriptAndRecoveryGuards();
10361069
testNonBlockingStartupAndForcedReinstallWiring();
10371070
testWurstProcessMatching();
10381071
await testModelThumbnailRequestsTexturesByDefault();

src/features/assetLinks.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ ${ICON_INLINE_CSS}
299299
var items = (initial.tabs[activeTab] || []);
300300
if (!query) return items.slice(0, 500);
301301
return items.map(function (item, index) {
302-
return { item: item, index: index, score: assetSearchScore(query, item.label, item.value, item.detail) };
302+
return { item: item, index: index, score: assetSearchScore(query, item.label, item.value, item.detail, fuzzyMatch) };
303303
}).filter(function (entry) {
304304
return Number.isFinite(entry.score);
305305
}).sort(function (a, b) {
@@ -609,6 +609,26 @@ ${ICON_INLINE_CSS}
609609
if (!modelJob.pendingTextures || modelJob.pendingTextures.size === 0) scheduleModelCapture(0, 1);
610610
}
611611
});
612+
window.__wurstCodeAssetBrowserDebug = {
613+
search: function (value) {
614+
query = String(value || '');
615+
document.getElementById('search').value = query;
616+
render();
617+
},
618+
state: function () {
619+
return {
620+
activeTab: activeTab,
621+
query: query,
622+
results: list().slice(0, 50).map(function (item) {
623+
return {
624+
label: item.label,
625+
value: item.value,
626+
score: assetSearchScore(query, item.label, item.value, item.detail, fuzzyMatch)
627+
};
628+
})
629+
};
630+
}
631+
};
612632
render();
613633
document.getElementById('search').focus();
614634
})();

0 commit comments

Comments
 (0)