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 src/autocomplete/CompletionFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@ export class CompletionFormatter {
if (
item?.kind === CompletionItemKind.EnumMember ||
item?.kind === CompletionItemKind.Reference ||
item?.kind === CompletionItemKind.Constant ||
item?.kind === CompletionItemKind.Event
) {
return label;
Expand Down
1 change: 1 addition & 0 deletions src/autocomplete/CompletionRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,7 @@ export function createCompletionProviders(
core.syntaxTreeManager,
external.schemaRetriever,
core.documentManager,
external.featureFlags.get('Constants'),
),
);
completionProviderMap.set('ParameterTypeValue', new ParameterTypeValueCompletionProvider());
Expand Down
66 changes: 60 additions & 6 deletions src/autocomplete/IntrinsicFunctionArgumentCompletionProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import { pseudoParameterDocsMap } from '../artifacts/PseudoParameterDocs';
import { Context } from '../context/Context';
import { IntrinsicFunction, PseudoParameter, PseudoParametersSet, TopLevelSection } from '../context/ContextType';
import { getEntityMap } from '../context/SectionContextBuilder';
import { Mapping, Parameter, Resource } from '../context/semantic/Entity';
import { Constant, Mapping, Parameter, Resource } from '../context/semantic/Entity';
import { EntityType } from '../context/semantic/SemanticTypes';
import { SyntaxTree } from '../context/syntaxtree/SyntaxTree';
import { SyntaxTreeManager } from '../context/syntaxtree/SyntaxTreeManager';
import { DocumentManager } from '../document/DocumentManager';
import { FeatureFlag } from '../featureFlag/FeatureFlagI';
import { SchemaRetriever } from '../schema/SchemaRetriever';
import { LoggerFactory } from '../telemetry/LoggerFactory';
import { Measure } from '../telemetry/TelemetryDecorator';
Expand Down Expand Up @@ -54,6 +55,7 @@ export class IntrinsicFunctionArgumentCompletionProvider implements CompletionPr
private readonly syntaxTreeManager: SyntaxTreeManager,
private readonly schemaRetriever: SchemaRetriever,
private readonly documentManager: DocumentManager,
private readonly constantsFeatureFlag: FeatureFlag,
) {}

@Measure({ name: 'getCompletions' })
Expand Down Expand Up @@ -104,14 +106,19 @@ export class IntrinsicFunctionArgumentCompletionProvider implements CompletionPr
syntaxTree,
);

if (!parametersAndResourcesCompletions || parametersAndResourcesCompletions.length === 0) {
const constantsCompletions = this.getConstantsCompletions(syntaxTree);

const allCompletions = [
...this.pseudoParameterCompletionItems,
...(parametersAndResourcesCompletions ?? []),
...constantsCompletions,
];

if (allCompletions.length === this.pseudoParameterCompletionItems.length) {
return this.applyFuzzySearch(this.pseudoParameterCompletionItems, context.text);
}

return this.applyFuzzySearch(
[...this.pseudoParameterCompletionItems, ...parametersAndResourcesCompletions],
context.text,
);
return this.applyFuzzySearch(allCompletions, context.text);
}

private handleSubArguments(
Expand All @@ -125,6 +132,7 @@ export class IntrinsicFunctionArgumentCompletionProvider implements CompletionPr
syntaxTree,
);
const getAttCompletions = this.getGetAttCompletions(syntaxTree, context.logicalId);
const constantsCompletions = this.getConstantsCompletions(syntaxTree, true);

const baseItems = [...this.pseudoParameterCompletionItems];
if (parametersAndResourcesCompletions && parametersAndResourcesCompletions.length > 0) {
Expand All @@ -133,6 +141,9 @@ export class IntrinsicFunctionArgumentCompletionProvider implements CompletionPr
if (getAttCompletions.length > 0) {
baseItems.push(...getAttCompletions);
}
if (constantsCompletions.length > 0) {
baseItems.push(...constantsCompletions);
}

// Handle ${} parameter substitution context detection
const subText = this.getTextForSub(params.textDocument.uri, params.position, context);
Expand Down Expand Up @@ -246,6 +257,49 @@ export class IntrinsicFunctionArgumentCompletionProvider implements CompletionPr
return completionItems;
}

private getConstantsAsCompletionItems(
constantsMap: ReadonlyMap<string, Context>,
stringOnly: boolean = false,
): CompletionItem[] {
const completionItems: CompletionItem[] = [];
for (const [constantName, context] of constantsMap) {
const constant = context.entity as Constant;

if (stringOnly && typeof constant.value !== 'string') {
continue;
}

const valuePreview =
typeof constant.value === 'string'
? constant.value
: typeof constant.value === 'object'
? '[Object]'
: String(constant.value);

completionItems.push(
createCompletionItem(constantName, CompletionItemKind.Constant, {
detail: `Constant`,
documentation: `Value: ${valuePreview}`,
}),
);
}

return completionItems;
}

private getConstantsCompletions(syntaxTree: SyntaxTree, stringOnly: boolean = false): CompletionItem[] {
if (!this.constantsFeatureFlag.isEnabled()) {
return [];
}

const constantsMap = getEntityMap(syntaxTree, TopLevelSection.Constants);
if (!constantsMap || constantsMap.size === 0) {
return [];
}

return this.getConstantsAsCompletionItems(constantsMap, stringOnly);
}

private shouldIncludeResourceCompletions(context: Context): boolean {
// Only provide resource completions in Resources and Outputs sections
return context.section === TopLevelSection.Resources || context.section === TopLevelSection.Outputs;
Expand Down
10 changes: 5 additions & 5 deletions tst/resources/templates/constants.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"Transform": "AWS::LanguageExtensions",
"Constants": {
"foo": "bar",
"sub": "${Const::foo}-abc-${AWS::AccountId}",
"sub": "${foo}-abc-${AWS::AccountId}",
"obj": {
"TestObject": {
"A": "b"
Expand All @@ -15,21 +15,21 @@
"Type": "AWS::S3::Bucket",
"Metadata": {
"Test": {
"Fn::Sub": "${Const::sub}-xyz"
"Fn::Sub": "${sub}-xyz"
},
"TestObj": {
"Ref": "Const::obj"
"Ref": "obj"
}
},
"Properties": {
"BucketName": {
"Fn::Sub": "${Const::foo}"
"Fn::Sub": "${foo}"
},
"Tags": [
{
"Key": "Environment",
"Value": {
"Ref": "Const::foo"
"Ref": "foo"
}
}
]
Expand Down
11 changes: 5 additions & 6 deletions tst/resources/templates/constants.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Transform: AWS::LanguageExtensions

Constants:
foo: bar
sub: "${Const::foo}-abc-${AWS::AccountId}"
sub: "${foo}-abc-${AWS::AccountId}"
obj:
TestObject:
A: b
Expand All @@ -12,16 +12,15 @@ Resources:
Bucket:
Type: AWS::S3::Bucket
Metadata:
Test: !Sub ${Const::sub}-xyz
TestObj: !Ref Const::obj
Test: !Sub ${sub}-xyz
TestObj: !Ref obj
Properties:
BucketName: !Sub ${Const::foo}
BucketName: !Sub ${foo}
Tags:
- Key: Environment
Value: !Ref Const::foo
Value: !Ref foo

PersonalS3:
Type: AWS::S3::Bucket
Properties:
BucketName: my-personal-bucket

41 changes: 39 additions & 2 deletions tst/unit/autocomplete/CompletionRouter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { docPosition, Templates } from '../../utils/TemplateUtils';
describe('CompletionRouter', () => {
const mockComponents = createMockComponents();

mockComponents.external.featureFlags.get.returns({ isEnabled: () => false, describe: () => 'mock' });
mockComponents.external.featureFlags.get.returns({ isEnabled: () => true, describe: () => 'mock feature flags' });

const completionRouter = CompletionRouter.create(
mockComponents.core,
Expand Down Expand Up @@ -103,7 +103,40 @@ describe('CompletionRouter', () => {
const regularSections = result!.items.filter((item) => item.kind === CompletionItemKind.Class);

// Check that we have the expected number of regular sections
expect(regularSections.length).toBe(10); // Should suggest all template sections for missing document
expect(regularSections.length).toBe(11); // Should suggest all template sections for missing document
});

test('should return less top-level sections when feature flag is disabled', async () => {
const mockComponentsWithDisabledFlag = createMockComponents();
mockComponentsWithDisabledFlag.external.featureFlags.get.returns({
isEnabled: () => false,
describe: () => 'Constants feature flag',
});

const completionRouterWithDisabledFlag = CompletionRouter.create(
mockComponentsWithDisabledFlag.core,
mockComponentsWithDisabledFlag.external,
mockComponentsWithDisabledFlag.providers,
);

const mockContext = createTopLevelContext('Unknown', { text: '' });

mockComponentsWithDisabledFlag.contextManager.getContext.returns(mockContext);

const result = await completionRouterWithDisabledFlag.getCompletions(mockParams);

expect(result).toBeDefined();
expect(result?.isIncomplete).toBe(false);

// Filter to get only regular sections (not snippets)
const regularSections = result!.items.filter((item) => item.kind === CompletionItemKind.Class);

// Check that we have 10 sections (without Constants)
expect(regularSections.length).toBe(10);

// Verify Constants is not in the results
const constantsItem = regularSections.find((item) => item.label === 'Constants');
expect(constantsItem).toBeUndefined();
});

test('should return resource section provider given context entity type of Resource', () => {
Expand Down Expand Up @@ -201,6 +234,10 @@ describe('CompletionRouter', () => {
schemaRetriever: mockComponents.schemaRetriever,
settingsManager: mockSettingsManager,
});
mockTestComponents.external.featureFlags.get.returns({
isEnabled: () => true,
describe: () => 'mock feature flags',
});
const completionProviderMap = createCompletionProviders(
mockTestComponents.core,
mockTestComponents.external,
Expand Down
Loading
Loading