Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
da262fe
feat: enhance HTML validation with debounce and context handling
anesvijskij Dec 3, 2025
ed26314
fix: update snippet formatting for HTML completion provider
anesvijskij Dec 3, 2025
4b964e2
feat: add HTML screen validation command and related functionality
anesvijskij Dec 3, 2025
48f638d
feat: add TypeScript screen validation command and related functionality
anesvijskij Dec 4, 2025
f7ab95d
fix: reduce noise in HTML validation diagnostics by commenting out mi…
anesvijskij Dec 4, 2025
b674556
feat: implement caching for API calls in LayeredDataService to optimi…
anesvijskij Dec 4, 2025
3409c38
feat: enhance backend metadata handling with new action and view supp…
anesvijskij Dec 4, 2025
d9e29f9
feat: add link command completion support and related utility functions
anesvijskij Dec 4, 2025
b6c0bf3
feat: enable cancellation for HTML and TypeScript validation processes
anesvijskij Dec 4, 2025
0ea3810
feat: add support for double underscore fields in PXView diagnostics
anesvijskij Dec 4, 2025
ad35064
feat: enhance PXView field hover and validation features with backend…
anesvijskij Dec 4, 2025
c8711a0
feat: refactor project screen and TypeScript validation tests to impr…
anesvijskij Dec 4, 2025
76349c1
Merge remote-tracking branch 'origin/main' into improvements-after-me…
anesvijskij Dec 4, 2025
fcf338f
Update acumate-plugin/src/test/suite/projectScreenValidation.test.ts
anesvijskij Dec 4, 2025
f3c879a
feat: add workspace validation functions for HTML and TypeScript files
anesvijskij Dec 4, 2025
2111b6a
Merge branch 'improvements-after-meeting' of https://github.com/Acuma…
anesvijskij Dec 4, 2025
0779679
Merge remote-tracking branch 'origin/main' into improvements-after-me…
anesvijskij Dec 4, 2025
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
2 changes: 1 addition & 1 deletion acumate-plugin/package-lock.json

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

14 changes: 13 additions & 1 deletion acumate-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,16 @@
"command": "acumate.dropCache",
"title": "Drop Local Cache",
"category": "AcuMate"
},
{
"command": "acumate.validateScreens",
"title": "Validate Screens (HTML)",
"category": "AcuMate"
},
{
"command": "acumate.validateTypeScriptScreens",
"title": "Validate Screens (TypeScript)",
"category": "AcuMate"
}
],
"menus": {
Expand Down Expand Up @@ -164,7 +174,9 @@
"watch": "tsc -watch -p ./",
"pretest": "npm run compile && npm run lint",
"lint": "eslint src",
"test": "vscode-test"
"test": "vscode-test",
"validate:screens": "node ./scripts/validate-screens.js",
"validate:screens:ts": "node ./scripts/validate-ts-screens.js"
},
"devDependencies": {
"@types/mocha": "^10.0.9",
Expand Down
10 changes: 9 additions & 1 deletion acumate-plugin/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,10 @@ The **AcuMate** extension for Visual Studio Code offers a range of powerful feat
- The same metadata powers TypeScript diagnostics, warning when a `graphType` string does not match any graph returned by the backend so you can catch typos before running the UI.
- The `@featureInstalled("FeatureName")` decorator gains backend-driven IntelliSense as you type the feature name and raises diagnostics when a missing/disabled feature is referenced, helping ensure feature-gated screens follow the site configuration.
- Validates `@linkCommand("ActionName")` decorators on PXFieldState members, ensuring the referenced PXAction exists on the backend graph (case-insensitive comparison).
- While editing a `@linkCommand("...")` decorator, AcuMate now surfaces completion items sourced from the backend action list so you can insert the exact action name without leaving the editor.
- Screen extension TypeScript files (under `.../extensions/...`) automatically reuse the parent screen's `@graphInfo` metadata so backend validations, completions, and linkCommand checks keep working even when the extension file lacks its own decorator.
- Hovering PXView field declarations displays the backend field’s display name, raw name, type, and default control, mirroring the HTML hover experience described below.
- Hovering PXView field declarations displays the backend field’s display name, raw name, type, and default control. Hovering PXView properties shows cache type/name metadata, and hovering PXActionState members surfaces their backend display names so you can confirm bindings at a glance.
- Fields whose names contain a double underscore (e.g., `__CustomField`) are treated as intentionally custom and are skipped by `graphInfo` diagnostics to avoid noise while prototyping.

### HTML Features

Expand Down Expand Up @@ -112,10 +114,16 @@ The **AcuMate** extension provides several commands to streamline development ta
| `acumate.watchCurrentScreen` | **Watch Current Screen** | Watches the currently active screen for changes and rebuilds as needed. |
| `acumate.repeatLastBuildCommand` | **Repeat Last Build Command** | Repeats the last executed build command, useful for quick iterations. |
| `acumate.dropCache` | **Drop Local Cache** | Clears the local cache, ensuring that the next build retrieves fresh data from the backend. |
| `acumate.validateScreens` | **Validate Screens (HTML)** | Scans every `.html` under `src/screens` (or a folder you choose), reports progress with a cancellable notification, and logs validator diagnostics to the **AcuMate Validation** output channel without failing on warnings. |
| `acumate.validateTypeScriptScreens`| **Validate Screens (TypeScript)** | Iterates through screen `.ts` files, runs the backend-powered `graphInfo` validator with cancellable progress, and streams any warnings/errors to the **AcuMate Validation** output channel so you can review them without interrupting development. |

### Quality & CI

1. **Automated Tests**
- Run `npm test` locally to compile, lint, and execute the VS Code integration suites (metadata, HTML providers, validator, scaffolding, build commands).
- The GitHub Actions workflow in `.github/workflows/ci.yml` performs `npm ci` + `npm test` for every pull request (regardless of branch) and on pushes to `main`, using a Node 18.x / 20.x matrix to catch regressions before and after merges.
2. **Project Screen Validation**
- Inside VS Code, run **AcuMate: Validate Screens (HTML)** to queue the validator against all HTML files beneath `src/screens` (or any folder you input). A cancellable progress notification tracks the run, and results are aggregated in the **AcuMate Validation** output channel so you can inspect warnings without breaking your workflow.
- For TypeScript coverage, run **AcuMate: Validate Screens (TypeScript)** to traverse the same folder structure, execute `collectGraphInfoDiagnostics` for each screen `.ts`, and summarize backend metadata mismatches in the output channel (requires `acuMate.useBackend = true`). This command is also cancellable so you can stop long-running validations instantly.
- From the CLI, run `npm run validate:screens` (HTML) or `npm run validate:screens:ts` (TypeScript) to execute the same scans headlessly via the VS Code test runner. Override the roots with `SCREEN_VALIDATION_ROOT` or `TS_SCREEN_VALIDATION_ROOT` environment variables when needed. Both scripts log summaries instead of failing on warnings, ensuring automated runs focus on catching crashes.

18 changes: 18 additions & 0 deletions acumate-plugin/scripts/validate-screens.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const { spawn } = require('child_process');
const path = require('path');

const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
const repoRoot = path.resolve(__dirname, '..');

const child = spawn(npmCmd, ['test'], {
cwd: repoRoot,
env: {
...process.env,
SCREEN_VALIDATION_ROOT: process.env.SCREEN_VALIDATION_ROOT || 'src/screens'
},
stdio: 'inherit'
});

child.on('exit', code => {
process.exit(code ?? 1);
});
18 changes: 18 additions & 0 deletions acumate-plugin/scripts/validate-ts-screens.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const { spawn } = require('child_process');
const path = require('path');

const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
const repoRoot = path.resolve(__dirname, '..');

const child = spawn(npmCmd, ['test'], {
cwd: repoRoot,
env: {
...process.env,
TS_SCREEN_VALIDATION_ROOT: process.env.TS_SCREEN_VALIDATION_ROOT || 'src/screens'
},
stdio: 'inherit'
});

child.on('exit', code => {
process.exit(code ?? 1);
});
60 changes: 51 additions & 9 deletions acumate-plugin/src/api/layered-data-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import { FeatureModel } from "../model/FeatureModel";

export class LayeredDataService implements IAcuMateApiClient {

private inflightGraphs?: Promise<GraphModel[] | undefined>;
private inflightFeatures?: Promise<FeatureModel[] | undefined>;
private readonly inflightStructures = new Map<string, Promise<GraphStructure | undefined>>();

constructor(private cacheService: CachedDataService, private apiService: AcuMateApiClient) {

}
Expand All @@ -18,9 +22,21 @@ export class LayeredDataService implements IAcuMateApiClient {
return cachedResult;
}

const apiResult = await this.apiService.getGraphs();
this.cacheService.store(GraphAPICache, apiResult);
return apiResult;
if (this.inflightGraphs) {
return this.inflightGraphs;
}

this.inflightGraphs = this.apiService
.getGraphs()
.then(result => {
this.cacheService.store(GraphAPICache, result);
return result;
})
.finally(() => {
this.inflightGraphs = undefined;
});

return this.inflightGraphs;

}

Expand All @@ -30,9 +46,23 @@ export class LayeredDataService implements IAcuMateApiClient {
return cachedResult;
}

const apiResult = await this.apiService.getGraphStructure(graphName);
this.cacheService.store(GraphAPIStructureCachePrefix + graphName, apiResult);
return apiResult;
const existing = this.inflightStructures.get(graphName);
if (existing) {
return existing;
}

const pending = this.apiService
.getGraphStructure(graphName)
.then(result => {
this.cacheService.store(GraphAPIStructureCachePrefix + graphName, result);
return result;
})
.finally(() => {
this.inflightStructures.delete(graphName);
});

this.inflightStructures.set(graphName, pending);
return pending;
}

async getFeatures(): Promise<FeatureModel[] | undefined> {
Expand All @@ -41,8 +71,20 @@ export class LayeredDataService implements IAcuMateApiClient {
return cachedResult;
}

const apiResult = await this.apiService.getFeatures();
this.cacheService.store(FeaturesCache, apiResult);
return apiResult;
if (this.inflightFeatures) {
return this.inflightFeatures;
}

this.inflightFeatures = this.apiService
.getFeatures()
.then(result => {
this.cacheService.store(FeaturesCache, result);
return result;
})
.finally(() => {
this.inflightFeatures = undefined;
});

return this.inflightFeatures;
}
}
35 changes: 31 additions & 4 deletions acumate-plugin/src/backend-metadata-utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { GraphStructure } from './model/graph-structure';
import { Field, View } from './model/view';
import { Action, Field, View } from './model/view';

export interface BackendFieldMetadata {
fieldName: string;
Expand All @@ -14,6 +14,12 @@ export interface BackendViewMetadata {
fields: Map<string, BackendFieldMetadata>;
}

export interface BackendActionMetadata {
actionName: string;
normalizedName: string;
action: Action;
}

export function normalizeMetaName(value: string | undefined): string | undefined {
if (typeof value !== 'string') {
return undefined;
Expand All @@ -25,14 +31,35 @@ export function normalizeMetaName(value: string | undefined): string | undefined

export function buildBackendActionSet(structure: GraphStructure | undefined): Set<string> {
const actions = new Set<string>();
const map = buildBackendActionMap(structure);
for (const key of map.keys()) {
actions.add(key);
}
return actions;
}

export function buildBackendActionMap(structure: GraphStructure | undefined): Map<string, BackendActionMetadata> {
const actions = new Map<string, BackendActionMetadata>();
if (!structure?.actions) {
return actions;
}

for (const action of structure.actions) {
const normalized = normalizeMetaName(action?.name);
if (normalized) {
actions.add(normalized);
if (!action) {
continue;
}

const normalized = normalizeMetaName(action.name);
if (!normalized) {
continue;
}

if (!actions.has(normalized)) {
actions.set(normalized, {
actionName: action.name ?? normalized,
normalizedName: normalized,
action
});
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { getAvailableGraphs } from '../services/graph-metadata-service';
import { getAvailableFeatures } from '../services/feature-metadata-service';
import { getGraphTypeLiteralAtPosition } from '../typescript/graph-info-utils';
import { getFeatureInstalledLiteralAtPosition } from '../typescript/feature-installed-utils';
import { getLinkCommandLiteralAtPosition } from '../typescript/link-command-utils';
import { buildBackendViewMap, normalizeMetaName } from '../backend-metadata-utils';

export async function provideTSCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken, context: vscode.CompletionContext): Promise<vscode.CompletionItem[] | undefined> {
Expand All @@ -32,6 +33,11 @@ export async function provideTSCompletionItems(document: vscode.TextDocument, po
return featureInstalledCompletions;
}

const linkCommandCompletions = await provideLinkCommandCompletions(document, position, sourceFile, documentText);
if (linkCommandCompletions?.length) {
return linkCommandCompletions;
}

let activeClassName: string | undefined;
let activeClassKind: 'PXScreen' | 'PXView' | undefined;

Expand Down Expand Up @@ -279,5 +285,50 @@ async function provideFeatureInstalledCompletions(
items.push(item);
}

return items.length ? items : undefined;
}

async function provideLinkCommandCompletions(
document: vscode.TextDocument,
position: vscode.Position,
sourceFile: ts.SourceFile,
documentText: string
): Promise<vscode.CompletionItem[] | undefined> {
const offset = document.offsetAt(position);
const literalInfo = getLinkCommandLiteralAtPosition(sourceFile, offset);
if (!literalInfo) {
return undefined;
}

const graphName = tryGetGraphType(documentText) ?? tryGetGraphTypeFromExtension(document.fileName);
if (!graphName || !AcuMateContext.ApiService) {
return undefined;
}

const graphStructure = await AcuMateContext.ApiService.getGraphStructure(graphName);
if (!graphStructure?.actions?.length) {
return undefined;
}

const range = getStringContentRange(document, literalInfo.literal);
const items: vscode.CompletionItem[] = [];
for (const action of graphStructure.actions) {
if (!action?.name) {
continue;
}

const item = new vscode.CompletionItem(action.name, vscode.CompletionItemKind.EnumMember);
item.insertText = action.name;
item.range = range;
item.sortText = action.name.toLowerCase();
item.detail = action.displayName ? `${action.name} (${action.displayName})` : action.name;
const docLines = [`PXAction from graph ${graphName}.`];
if (action.displayName) {
docLines.push(`Display name: ${action.displayName}`);
}
item.documentation = new vscode.MarkdownString(docLines.join('\n\n'));
items.push(item);
}

return items.length ? items : undefined;
}
Loading