Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
50c697f
include-aware qp-include field validation
nesvizhskie May 8, 2026
03c5df9
include-aware qp-include definition navigation
nesvizhskie May 8, 2026
0ef1162
support remove selector navigation
nesvizhskie May 8, 2026
e0c49ea
Merge branch 'main' into bugifx/qp-include-field-validation-wip
nesvizhskie May 11, 2026
f9a587e
Resolve selectors against extension dependencies
nesvizhskie May 11, 2026
0b32444
Resolve include selector target view scopes
nesvizhskie May 12, 2026
f837aec
Honor inherited control config properties
nesvizhskie May 12, 2026
deb617e
Allow id-less shell qp controls
nesvizhskie May 12, 2026
34f0fe3
Allow id-less longrun indicator
nesvizhskie May 12, 2026
fa3d272
Ignore mustache-templated HTML bindings
nesvizhskie May 12, 2026
6e72a0e
Ignore unbound field names
nesvizhskie May 12, 2026
c7e4d21
Allow id-less qp shell components
nesvizhskie May 12, 2026
dccfe6b
Resolve state actions from view scope
nesvizhskie May 13, 2026
f1cb9df
Resolve duplicate view classes by import source
nesvizhskie May 13, 2026
c84db42
Resolve include child selectors with host mixins
nesvizhskie May 13, 2026
018f91b
Complete include child fields from host mixins
nesvizhskie May 13, 2026
d059956
Resolve HTML field hover contexts
nesvizhskie May 13, 2026
358ef75
Share HTML field context resolution
nesvizhskie May 13, 2026
100207a
Accept views with unresolved imported bases
nesvizhskie May 13, 2026
b96ab23
Treat non-button state bindings as fields
nesvizhskie May 13, 2026
178458f
Resolve qualified button action bindings
nesvizhskie May 13, 2026
81360ba
Resolve include selectors with parameters
nesvizhskie May 13, 2026
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
121 changes: 107 additions & 14 deletions acumate-plugin/src/providers/html-completion-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,18 @@ import {
getRelatedTsFiles,
loadClassInfosFromFiles,
CollectedClassInfo,
createClassInfoLookup,
resolveViewBinding,
ClassPropertyInfo,
filterScreenLikeClasses,
collectActionProperties,
extractConfigPropertyNames,
filterClassesBySource,
} from '../utils';
import {
parseDocumentDom,
findNodeAtOffset,
elevateToElementNode,
getAttributeContext,
findParentViewName,
isActionStateBindTag,
} from './html-shared';
import {
ClientControlMetadata,
Expand All @@ -27,6 +25,16 @@ import { getScreenTemplates } from '../services/screen-template-service';
import { getIncludeMetadata, IncludeMetadata } from '../services/include-service';
import { BackendFieldMetadata, normalizeMetaName } from '../backend-metadata-utils';
import { loadBackendFieldsForView } from './html-backend-utils';
import { getBaseScreenDocument } from '../services/screen-html-service';
import {
HtmlIncludeFieldContext,
createHtmlFieldMetadataContext,
getFieldPropertiesFromViews,
getIncludeFieldContext,
getParentOrSelectorViewName,
getViewNameFromCustomizationSelectors,
resolveTemplateValue,
} from '../services/html-field-context-service';

// Registers completions so HTML view bindings stay in sync with PX metadata.
export function registerHtmlCompletionProvider(context: vscode.ExtensionContext) {
Expand Down Expand Up @@ -103,13 +111,8 @@ export class HtmlCompletionProvider implements vscode.CompletionItemProvider {
return undefined;
}

const relevantClassInfos = filterClassesBySource(classInfos, tsFilePaths);
if (!relevantClassInfos.length) {
return undefined;
}

const classInfoLookup = createClassInfoLookup(classInfos);
const screenClasses = filterScreenLikeClasses(relevantClassInfos);
const metadataContext = createHtmlFieldMetadataContext(classInfos, tsFilePaths);
const { classInfoLookup, screenClasses } = metadataContext;
// Completions are sourced from the same metadata as validation/definitions to keep behavior consistent.

if (attributeContext.attributeName === 'view.bind') {
Expand All @@ -121,7 +124,9 @@ export class HtmlCompletionProvider implements vscode.CompletionItemProvider {
}

if (attributeContext.attributeName === 'state.bind') {
return this.createActionCompletions(screenClasses);
return isActionStateBindTag(attributeContext.tagName)
? this.createActionCompletions(screenClasses, classInfoLookup, elementNode, attributeContext.value)
: this.createControlStateCompletions(attributeContext.value, screenClasses, classInfoLookup);
}

if (attributeContext.attributeName === 'control-state.bind' && attributeContext.tagName === 'qp-field') {
Expand All @@ -130,7 +135,35 @@ export class HtmlCompletionProvider implements vscode.CompletionItemProvider {

if (attributeContext.attributeName === 'name' && attributeContext.tagName === 'field') {
// Field completions are scoped to the PXView resolved from the surrounding markup.
const viewName = findParentViewName(elementNode);
const includeContext = getIncludeFieldContext({
documentPath: document.uri.fsPath,
elementNode,
hostTsFilePaths: tsFilePaths,
workspaceRoots: vscode.workspace.workspaceFolders?.map(folder => folder.uri.fsPath),
});
if (includeContext) {
const hostSelectorViewName = resolveTemplateValue(
getViewNameFromCustomizationSelectors(elementNode, includeContext.templateDocument),
includeContext.parameterValues
);
const hostSelectorResolution = hostSelectorViewName
? resolveViewBinding(hostSelectorViewName, screenClasses, classInfoLookup)
: undefined;
if (hostSelectorResolution?.viewClass) {
const backendFields = await loadBackendFieldsForView(hostSelectorViewName!, screenClasses);
return this.createFieldCompletions(hostSelectorResolution.viewClass.properties, backendFields);
}

const items = this.createIncludeFieldNameCompletions(elementNode, includeContext);
if (items?.length) {
return items;
}
}

const viewName = getParentOrSelectorViewName(
elementNode,
getBaseScreenDocument(document.uri.fsPath)
);
if (!viewName) {
return undefined;
}
Expand Down Expand Up @@ -366,6 +399,22 @@ export class HtmlCompletionProvider implements vscode.CompletionItemProvider {
return items.length ? items : undefined;
}

private createIncludeFieldNameCompletions(
elementNode: any,
context: HtmlIncludeFieldContext
): vscode.CompletionItem[] | undefined {
const selectorViewName = getViewNameFromCustomizationSelectors(elementNode, context.templateDocument);
const viewName = resolveTemplateValue(selectorViewName, context.parameterValues);
if (viewName) {
const resolution = resolveViewBinding(viewName, context.screenClasses, context.classInfoLookup);
const viewClass = resolution?.viewClass;
return viewClass ? this.createFieldCompletions(viewClass.properties) : undefined;
}

const fields = getFieldPropertiesFromViews(context);
return fields.size ? this.createFieldCompletions(fields) : undefined;
}

private isInsideDataFeed(node: any): boolean {
let current = node?.parent ?? node?.parentNode;
while (current) {
Expand Down Expand Up @@ -590,14 +639,58 @@ export class HtmlCompletionProvider implements vscode.CompletionItemProvider {
return items;
}

private createActionCompletions(screenClasses: CollectedClassInfo[]): vscode.CompletionItem[] {
private createActionCompletions(
screenClasses: CollectedClassInfo[],
classInfoLookup: Map<string, CollectedClassInfo>,
elementNode: any,
currentValue?: string
): vscode.CompletionItem[] {
const normalizedPrefix = (currentValue ?? '').trim().toLowerCase();
const actionMap = collectActionProperties(screenClasses);
const items: vscode.CompletionItem[] = [];
actionMap.forEach((property, name) => {
const seen = new Set<string>();
const addAction = (name: string, property: ClassPropertyInfo) => {
if (seen.has(name)) {
return;
}
if (normalizedPrefix && !name.toLowerCase().startsWith(normalizedPrefix)) {
return;
}
seen.add(name);
const item = new vscode.CompletionItem(name, vscode.CompletionItemKind.Function);
item.detail = property.typeName ?? 'PXActionState';
items.push(item);
};

actionMap.forEach((property, name) => {
addAction(name, property);
});

const viewName = findParentViewName(elementNode);
const viewClass = viewName
? resolveViewBinding(viewName, screenClasses, classInfoLookup)?.viewClass
: undefined;
viewClass?.properties.forEach((property, name) => {
if (property.kind === 'action') {
addAction(name, property);
}
});

for (const screenClass of screenClasses) {
for (const [propertyName, property] of screenClass.properties) {
if (property.kind !== 'view' && property.kind !== 'viewCollection') {
continue;
}

const viewClass = resolveViewBinding(propertyName, screenClasses, classInfoLookup)?.viewClass;
viewClass?.properties.forEach((viewProperty, actionName) => {
if (viewProperty.kind === 'action') {
addAction(`${propertyName}.${actionName}`, viewProperty);
}
});
}
}

return items;
}

Expand Down
Loading
Loading