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: 1 addition & 0 deletions .vscodeignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ node_modules/**
scripts/**
e2e/**
wc3data/**
resources/wc3-knowledge-base.json

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep the bundled knowledge-base fallback

Do not exclude this resource from the VSIX while loadCompilerKnowledgeBase() still uses it as the fallback when the installed compiler JAR is missing, busy, old, or lacks the knowledge-base entry. In a fresh/offline or standalone installation without usable WC3 game data, the packaged extension now returns undefined instead of loading the bundled metadata, leaving object-editor fields and object references without their schemas, labels, and catalog data.

Useful? React with 👍 / 👎.

test/**
tests/**
__tests__/**
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 32 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"type": "git",
"url": "https://github.com/wurstscript/wurst4vscode.git"
},
"version": "0.12.11",
"version": "0.12.12",
"publisher": "peterzeller",
"engines": {
"vscode": "^1.109.0"
Expand All @@ -27,7 +27,11 @@
"onCustomEditor:wurst.objModPreview",
"onCustomEditor:wurst.soundPreview",
"onCommand:wurst.previewMap",
"onCommand:wurst.reportIssue"
"onCommand:wurst.reportIssue",
"onCommand:wurst.showDiagnosticsActions",
"onCommand:wurst.openWurstHome",
"onCommand:wurst.copyDiagnostics",
"onCommand:wurst.showLogs"
],
"main": "./dist/extension",
"browser": "./dist/web/extension.js",
Expand Down Expand Up @@ -694,7 +698,22 @@
},
{
"command": "wurst.showLogs",
"title": "Open VSCode output panel for Wurst logs",
"title": "Open Wurst output",
"category": "wurst"
},
{
"command": "wurst.showDiagnosticsActions",
"title": "Wurst: Show diagnostics actions",
"category": "wurst"
},
{
"command": "wurst.openWurstHome",
"title": "Wurst: Open Wurst home",
"category": "wurst"
},
{
"command": "wurst.copyDiagnostics",
"title": "Wurst: Copy diagnostics",
"category": "wurst"
},
{
Expand Down Expand Up @@ -790,6 +809,14 @@
"scope": "resource",
"description": "Optional path to a .ttf font, relative to the workspace root, for custom glyphs in WC3 object-editor tooltip previews. Leave empty to use the default VS Code font."
},
"wurst.objModTooltipWidth": {
"type": "number",
"default": 280,
"minimum": 160,
"maximum": 1200,
"scope": "resource",
"description": "Maximum width in CSS pixels of WC3 object-editor tooltip preview boxes. This can be set independently per workspace or workspace folder."
},
"wurst.gameExePath": {
"type": "string",
"default": "",
Expand Down Expand Up @@ -850,8 +877,9 @@
"lint": "eslint .",
"compile-web": "webpack",
"watch-web": "webpack --watch",
"test": "npm run test:fuzzy && npm run test:image-decoders && npm run test:webview",
"test": "npm run test:fuzzy && npm run test:image-decoders && npm run test:diagnostics && npm run test:webview",
"test:image-decoders": "node ./scripts/test-image-decoders.js",
"test:diagnostics": "node ./scripts/test-diagnostics.js",
"test:vsix-contents": "node ./scripts/test-vsix-contents.js",
"sync:wc3-knowledge-base": "node ./scripts/sync-wc3-knowledge-base.js",
"test:webview": "node ./scripts/test-webview.js",
Expand Down
91 changes: 91 additions & 0 deletions scripts/objmod-thumbnail-e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
* WURST_OBJMOD_E2E_SEARCH optional model-catalog search query
* WURST_OBJMOD_E2E_MAX_MS max warm per-thumbnail lifecycle, default 200ms
* WURST_OBJMOD_E2E_TIMEOUT_MS total wait timeout, default 90000
* WURST_OBJMOD_E2E_FONT_ONLY stop after verifying a configured tooltip font
*/

const assert = require('assert');
Expand Down Expand Up @@ -50,6 +51,7 @@ const maxThumbnailMs = Number(process.env.WURST_OBJMOD_E2E_MAX_MS || 200);
// wait up to 30 seconds before creating their first renderer. Leave enough time after that for
// extension activation, CASC catalog loading, and the uncached worker renders.
const timeoutMs = Number(process.env.WURST_OBJMOD_E2E_TIMEOUT_MS || 90000);
const fontOnly = process.env.WURST_OBJMOD_E2E_FONT_ONLY === '1';

function writeGeneratedObjmodFixture() {
const { serializeObjMod } = require('casc-ts/formats');
Expand Down Expand Up @@ -289,6 +291,14 @@ class CdpClient {
this.nextId = 1;
this.pending = new Map();
this.listeners = new Map();
this.browserDiagnostics = [];
this.on('Log.entryAdded', ({ entry }) => {
if (entry?.text) this.browserDiagnostics.push(entry.text);
});
this.on('Runtime.consoleAPICalled', ({ type, args }) => {
const message = (args || []).map((arg) => arg.value ?? arg.description ?? '').join(' ');
if (message) this.browserDiagnostics.push(`${type}: ${message}`);
});
}

async connect() {
Expand Down Expand Up @@ -369,6 +379,7 @@ async function waitForWebviewContext(client) {
if (attached?.sessionId) {
attachedTargets.add(target.targetId);
await client.send('Runtime.enable', {}, attached.sessionId);
await client.send('Log.enable', {}, attached.sessionId).catch(() => undefined);
}
} catch {
attachedTargets.add(target.targetId);
Expand Down Expand Up @@ -413,6 +424,70 @@ async function waitForEval(client, sessionId, contextId, expression, predicate,
throw new Error(`Timed out waiting for ${label}. Last value: ${JSON.stringify(last)}`);
}

async function assertTooltipFont(client, sessionId, contextId) {
const status = await evalInContext(client, sessionId, contextId, `(async function () {
var family = 'WurstProjectTooltip';
var face = Array.from(document.fonts).find(function (candidate) { return candidate.family === family; });
if (!face) return { configured: false };
var resource = {};
try {
var rules = Array.from(document.styleSheets).flatMap(function (sheet) { return Array.from(sheet.cssRules || []); });
var fontRule = rules.find(function (rule) { return rule.cssText && rule.cssText.indexOf(family) >= 0 && rule.style && rule.style.src; });
resource.src = fontRule ? fontRule.style.src : '';
var match = resource.src.match(/url\\(["']?([^"')]+)["']?\\)/);
if (match) {
var response = await fetch(match[1]);
var bytes = await response.arrayBuffer();
resource.ok = response.ok;
resource.status = response.status;
resource.type = response.type;
resource.bytes = bytes.byteLength;
resource.magic = Array.from(new Uint8Array(bytes.slice(0, 4)));
}
} catch (error) {
resource.error = String(error);
}
try {
var loadedFaces = await document.fonts.load('16px "WurstProjectTooltip"');
var target = document.querySelector('.tt-collapsed-box, .tt-preview');
var computed = target ? getComputedStyle(target).fontFamily : '';
var outside = Array.from(document.querySelectorAll('td.id, td.num, td.value, td.label, .cell-edit-val'))
.filter(function (candidate) { return !candidate.closest('.tt-collapsed-box, .tt-preview'); })
.slice(0, 50)
.map(function (candidate) {
return {
selector: candidate.tagName.toLowerCase() + '.' + candidate.className,
font: getComputedStyle(candidate).fontFamily,
text: String(candidate.textContent || '').trim().slice(0, 40),
};
});
return {
configured: true,
loaded: loadedFaces.some(function (candidate) { return candidate.family === family; }),
status: face.status,
computed: computed,
applied: target ? computed.indexOf(family) >= 0 : null,
editorFont: getComputedStyle(document.documentElement).getPropertyValue('--vscode-editor-font-family').trim(),
outsideUsingTooltipFont: outside.filter(function (candidate) { return candidate.font.indexOf(family) >= 0; }),
resource: resource,
};
} catch (error) {
return { configured: true, loaded: false, status: face.status, error: String(error), resource: resource };
}
})()`);
if (!status.configured) {
log('tooltip font not configured; font assertion skipped');
return;
}
status.diagnostics = client.browserDiagnostics.filter((message) => /font|ots/i.test(message)).slice(-20);
assert.equal(status.error, undefined, `configured tooltip font failed to load: ${JSON.stringify(status)}`);
assert.equal(status.loaded, true, `configured tooltip font did not produce a loaded face: ${JSON.stringify(status)}`);
assert.equal(status.status, 'loaded', `configured tooltip font has unexpected status: ${JSON.stringify(status)}`);
assert.equal(status.applied, true, `configured tooltip font is not applied to the tooltip box: ${JSON.stringify(status)}`);
assert.deepEqual(status.outsideUsingTooltipFont, [], `configured tooltip font leaked outside tooltip boxes: ${JSON.stringify(status)}`);
log(`tooltip font loaded and scoped (${status.computed}); editor font=${status.editorFont || '(default)'}`);
}

async function assertObjmodEditorBasics(client, sessionId, contextId) {
await evalInContext(client, sessionId, contextId, 'window.__wurstModelThumbDebug.forceNarrowLayout(true)');

Expand Down Expand Up @@ -574,6 +649,17 @@ async function assertObjmodEditorBasics(client, sessionId, contextId) {
assert.ok(customRows.some((row) => String(row.fieldId).toLowerCase() === 'anam' && row.overridden), 'custom object should keep modified fields');
}

await waitForEval(
client,
sessionId,
contextId,
'!!document.querySelector(".tt-collapsed-box, .tt-preview")',
(value) => value === true,
'rendered tooltip box',
15000,
);
await assertTooltipFont(client, sessionId, contextId);

await evalInContext(client, sessionId, contextId, 'window.__wurstModelThumbDebug.openModelAssetBrowser()');
await evalInContext(client, sessionId, contextId, 'window.__wurstModelThumbDebug.searchModelAssetBrowser("LordaeronTree")');
const assetState = await waitForEval(
Expand Down Expand Up @@ -692,6 +778,11 @@ async function main() {
const { sessionId, contextId } = await waitForWebviewContext(client);
log('asserting objmod editor basics');
await assertObjmodEditorBasics(client, sessionId, contextId);
if (fontOnly) {
log('font-only check passed');
passed = true;
return;
}
log('asserting model thumbnails');
await evalInContext(client, sessionId, contextId, 'window.__wurstModelThumbDebug.openModelAssetBrowser()');
if (searchQuery) {
Expand Down
35 changes: 35 additions & 0 deletions scripts/test-diagnostics.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
'use strict';

const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const ts = require('typescript');

const root = path.resolve(__dirname, '..');
const source = fs.readFileSync(path.join(root, 'src', 'features', 'diagnostics.ts'), 'utf8');
const js = ts.transpileModule(source, {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
}).outputText;
const mod = { exports: {} };
new Function('exports', 'module', 'require', js)(mod.exports, mod, require);

const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'wurst-diagnostics-'));
fs.mkdirSync(path.join(tempHome, 'logs'));
fs.writeFileSync(
path.join(tempHome, 'logs', 'languageServer.log'),
Array.from({ length: 125 }, (_, index) => `language-server-${index + 1}`).join('\n')
);

mod.exports.appendDiagnostic('WC3 data', 'PKExplode: invalid literal size byte 40\n at explode (pkware.ts:42:7)');
mod.exports.appendDiagnostic('MPQ', 'MPQ archive opened');
mod.exports.appendDiagnostic('Inline icons', 'thumb generation failed');
const report = mod.exports.buildDiagnosticsText(tempHome);

assert(report.includes('PKExplode: invalid literal size byte 40'));
assert(report.includes('at explode (pkware.ts:42:7)'));
assert(report.includes('MPQ archive opened'));
assert(report.includes('language-server-125'));
assert(!report.includes('language-server-25\n'));
assert(report.split('\n').filter((line) => line.includes('language-server-')).length === 100);
console.log('diagnostics tests passed (bounded tails and stack traces)');
6 changes: 5 additions & 1 deletion scripts/test-vsix-contents.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,13 @@ const forbidden = [
const leaked = files.filter((file) => forbidden.some((pattern) => pattern.test(file)));
assert.deepStrictEqual(leaked, [], `Test/development files would be packaged:\n${leaked.join('\n')}`);

for (const required of ['package.json', 'README.md', 'dist/extension.js', 'resources/wc3-knowledge-base.json']) {
for (const required of ['package.json', 'README.md', 'dist/extension.js']) {
assert(files.includes(required), `Required release file is missing: ${required}`);
}
assert(
!files.includes('resources/wc3-knowledge-base.json'),
'Compiler knowledge-base JSON must not be bundled in the extension',
);

const readme = fs.readFileSync(path.join(root, 'README.md'), 'utf8');
const readmeImages = [...readme.matchAll(/!\[[^\]]*\]\(([^)]+)\)/g)].map((match) => match[1]);
Expand Down
29 changes: 28 additions & 1 deletion scripts/test-webview.js
Original file line number Diff line number Diff line change
Expand Up @@ -915,11 +915,36 @@ function testObjModTooltipFontWiring() {
assert.ok(host.includes('webview.asWebviewUri(fontUri)'), 'project fonts must be converted to webview resource URIs');
assert.ok(host.includes('font-src ${context.webview.cspSource}'), 'objmod CSP must permit the project font resource');
assert.ok(host.includes('@font-face'), 'objmod should declare the project font for tooltip previews');
assert.ok(host.includes(".tt-collapsed-box,\n.tt-preview"), 'the custom font should be scoped to tooltip previews');
assert.ok(!host.includes("Buffer.from(bytes).toString('base64')"), 'project fonts should not be embedded as large data URLs');
assert.ok(host.includes(".tt-collapsed-box,\n.tt-preview {"), 'the custom font should be assigned only to tooltip boxes');
assert.ok(!host.includes('.tt-collapsed-box *'), 'the custom font should not use descendant-wide override selectors');
assert.ok(packageJson.includes('wurst.objModTooltipFont'), 'the tooltip font setting should be contributed by the extension');
assert.equal(packageData.contributes.configuration.properties['wurst.objModTooltipFont'].default, '', 'the tooltip font setting must default to disabled');
}

function testObjModTooltipWidthWiring() {
const host = fs.readFileSync(path.join(root, 'src/features/objModPreview.ts'), 'utf8');
const packageData = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
const setting = packageData.contributes.configuration.properties['wurst.objModTooltipWidth'];

assert.equal(setting.default, 280, 'tooltip width should preserve the existing default');
assert.equal(setting.minimum, 160, 'tooltip width should reject unusably narrow values');
assert.equal(setting.maximum, 1200, 'tooltip width should have a sensible upper bound');
assert.equal(setting.scope, 'resource', 'tooltip width should be remembered per workspace folder');
assert.ok(host.includes("get<number>(TOOLTIP_WIDTH_SETTING, DEFAULT_TOOLTIP_WIDTH_PX)"), 'objmod should read the configured tooltip width for the document resource');
assert.ok(host.includes('--wc3-tip-width: ${tooltipWidthPx}px;'), 'objmod should apply the configured width to tooltip preview CSS');
assert.ok(!host.includes('--wc3-tip-width: 280px;'), 'tooltip preview width should not remain hard-coded');
}

function testObjModTooltipPreviewHeaders() {
const fieldDisplay = fs.readFileSync(path.join(root, 'src/webview/objModEditor/fieldDisplay.ts'), 'utf8');
const detailsPanel = fs.readFileSync(path.join(root, 'src/webview/objModEditor/detailsPanel.ts'), 'utf8');

assert.ok(fieldDisplay.includes("replace(/^(?:unit|building)\\s*\\/\\s*/i, '')"), 'unit and building tooltip markers should be hidden in previews');
assert.ok(detailsPanel.includes('renderWc3Colors(original)'), 'tooltip editing should restore the unmodified raw value');
assert.ok(detailsPanel.includes('renderWc3Colors(tooltipPreviewText(value, isTooltipTemplateField(mod)))'), 'tooltip collapse should restore the cleaned preview only for tooltip fields');
}

function testObjModEditorTypeAndRecoveryGuards() {
const host = fs.readFileSync(path.join(root, 'src/features/objModPreview.ts'), 'utf8');
const webviewFiles = [
Expand Down Expand Up @@ -963,6 +988,8 @@ async function main() {
await testIssueReportingPrivacyAndDeduplication();
testObjModSaveCommitsFocusedEditor();
testObjModTooltipFontWiring();
testObjModTooltipWidthWiring();
testObjModTooltipPreviewHeaders();
console.log('webview harness tests passed');
}

Expand Down
8 changes: 7 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@ import { registerTriggerPreview } from './features/triggerPreview';
import { registerMapDataPreview } from './features/mapDataPreview';
import { registerMapPreview } from './features/mapPreview';
import { registerAgentsGuideOffer } from './features/agentsGuide';
import { registerWurstDiagnosticsCommands } from './features/commands';
import { openIssueReport } from './features/issueReporting';
import { registerCascDiagnosticsCommand } from './features/preview/cascStorage';
import { appendDiagnostic, formatDiagnosticError } from './features/diagnostics';

export async function activate(context: ExtensionContext) {
console.log('Wurst extension activated!');
Expand All @@ -47,6 +49,7 @@ export async function activate(context: ExtensionContext) {
context.subscriptions.push(registerMapPreview(context));
context.subscriptions.push(registerAgentsGuideOffer(context));
context.subscriptions.push(registerCascDiagnosticsCommand());
registerWurstDiagnosticsCommands(context);

registerBasicCommands(context);
openObjModE2eFixture();
Expand Down Expand Up @@ -89,6 +92,7 @@ function registerBasicCommands(context: ExtensionContext) {
await vscode.commands.executeCommand('workbench.action.reloadWindow');
} catch (e: any) {
if (e instanceof InstallCoordinationCancelledError) return;
appendDiagnostic('VS Code extension', `Install/update failed: ${formatDiagnosticError(e)}`);
vscode.window.showErrorMessage(`Install/Update failed: ${e?.message || e}`);
}
}),
Expand Down Expand Up @@ -126,6 +130,7 @@ function registerBasicCommands(context: ExtensionContext) {
try {
await createNewWurstProject();
} catch (e: any) {
appendDiagnostic('VS Code extension', `New project creation failed: ${formatDiagnosticError(e)}`);
vscode.window.showErrorMessage(`Failed to create Wurst project: ${e?.message ?? String(e)}`);
}
}),
Expand All @@ -148,7 +153,8 @@ async function startLanguageClientWhenWorkspaceIsOpen(context: ExtensionContext)
try {
await startLanguageClient(context);
} catch (err) {
console.error('Failed to start language client:', err);
appendDiagnostic('VS Code extension', `Failed to start language client: ${formatDiagnosticError(err)}`);
console.error('Failed to start language client:', formatDiagnosticError(err));
vscode.window.showWarningMessage(`Wurst language features disabled: ${err}`);
}
}
Loading
Loading