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
11 changes: 10 additions & 1 deletion src/autocomplete/CompletionRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,16 @@ export class CompletionRouter implements SettingsConfigurable, Closeable {
}

// Output Condition attribute: ['Outputs', 'LogicalId', 'Condition']
return context.matchPathWithLogicalId(TopLevelSection.Outputs, EntitySection.Condition);
if (context.matchPathWithLogicalId(TopLevelSection.Outputs, EntitySection.Condition)) {
return true;
}

// Condition key inside Properties: ['Resources', 'LogicalId', 'Properties', ..., 'Condition']
if (context.matchPathWithLogicalId(TopLevelSection.Resources, EntitySection.Properties)) {
return context.propertyPath.at(-1) === EntitySection.Condition;
}

return false;
}

private conditionUsageWithinIntrinsic(context: Context): boolean {
Expand Down
45 changes: 45 additions & 0 deletions src/autocomplete/IntrinsicFunctionArgumentCompletionProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,13 @@ export class IntrinsicFunctionArgumentCompletionProvider implements CompletionPr
params: CompletionParams,
syntaxTree: SyntaxTree,
): CompletionItem[] | undefined {
const intrinsicFunction = context.intrinsicContext.intrinsicFunction();

// Check if we're typing a key in the second argument (variable mapping object)
if (intrinsicFunction !== undefined && this.isInSubVariableMappingKey(context)) {
return this.getSubVariableCompletions(intrinsicFunction.args, context);
}

const parametersAndResourcesCompletions = this.getParametersAndResourcesAsCompletionItems(
context,
params,
Expand Down Expand Up @@ -179,6 +186,44 @@ export class IntrinsicFunctionArgumentCompletionProvider implements CompletionPr
return this.applyFuzzySearch(baseItems, context.text);
}

private isInSubVariableMappingKey(context: Context): boolean {
// Fn::Sub with array syntax: ["template ${Var}", {Var: value}]
// Check if propertyPath indicates we're in the second argument (index 1) as a key
const path = context.propertyPath;
const subIndex = path.indexOf(IntrinsicFunction.Sub);
if (subIndex === -1) {
return false;
}

// Path should be [..., 'Fn::Sub', 1, 'KeyName'] for typing a key in second arg
// Also require non-empty text to avoid false positives when position is invalid
return path[subIndex + 1] === 1 && path.length === subIndex + 3 && context.text.length > 0;
}

private getSubVariableCompletions(args: unknown, context: Context): CompletionItem[] {
if (!Array.isArray(args) || args.length === 0 || typeof args[0] !== 'string') {
return [];
}

const templateString = args[0];
const existingVars = args[1] && typeof args[1] === 'object' ? Object.keys(args[1] as object) : [];

// Extract ${VarName} patterns from template string
const varPattern = /\$\{([^}!.]+)}/g;
const variables = new Set<string>();
let match;
while ((match = varPattern.exec(templateString)) !== null) {
variables.add(match[1]);
}

// Filter out already defined variables and apply fuzzy search
const items = [...variables]
.filter((v) => !existingVars.includes(v))
.map((v) => createCompletionItem(v, CompletionItemKind.Variable, { context }));

return context.text.length > 0 ? this.fuzzySearch(items, context.text) : items;
}

private getParametersAndResourcesAsCompletionItems(
context: Context,
params: CompletionParams,
Expand Down
1 change: 1 addition & 0 deletions src/context/ContextType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export enum IntrinsicShortForms {

export enum EntitySection {
Condition = 'Condition',
Properties = 'Properties',
}

export const IntrinsicsUsingConditionKeyword: ReadonlyArray<IntrinsicFunction> = [
Expand Down
21 changes: 4 additions & 17 deletions tst/integration/autocomplete/Autocomplete.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -853,19 +853,12 @@ Resources:
content: `Version: !GetAtt L`,
position: { line: 239, character: 8 },
description:
'Suggest resource logical id when using Fn::GetAtt. Omit the resource being authored',
'Suggest resource logical ids matching L when using Fn::GetAtt. Omit the resource being authored',
verification: {
position: { line: 239, character: 26 },
expectation: CompletionExpectationBuilder.create()
.expectContainsItems([
'LaunchTemplate',
'PublicSubnet',
'WebSecurityGroup',
'BastionSecurityGroup',
'VPC',
])
.expectContainsItems(['LaunchTemplate', 'PublicSubnet'])
.expectExcludesItems(['AutoScalingGroup'])
.todo(`support autocomplete for Fn::GetAtt`)
.build(),
},
},
Expand Down Expand Up @@ -1544,13 +1537,8 @@ O`,
position: { line: 471, character: 64 },
description: 'suggest substitution variable in second arg of Fn::Sub based on first arg',
verification: {
position: { line: 474, character: 14 },
expectation: CompletionExpectationBuilder.create()
.expectItems(['Third'])
.todo(
`feature to suggest variables authored in Fn::Sub first arg while typing second arg`,
)
.build(),
position: { line: 473, character: 14 },
expectation: CompletionExpectationBuilder.create().expectItems(['Third']).build(),
},
},
],
Expand Down Expand Up @@ -3018,7 +3006,6 @@ Resources:
'IsProductionOrStaging',
])
.expectExcludesItems(['ComplexCondition', 'HasMultipleAZs'])
.todo('Not returning anything')
.build(),
},
},
Expand Down
9 changes: 3 additions & 6 deletions tst/integration/hover/Hover.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1087,9 +1087,8 @@ Resources:`,
verification: {
position: { line: 239, character: 30 },
expectation: HoverExpectationBuilder.create()
.expectStartsWith('**Resource:** LaunchTemplate')
.expectStartsWith('```typescript\n(resource) LaunchTemplate')
.expectContainsText(['LaunchTemplate', 'AWS::EC2::LaunchTemplate'])
.todo(`Returns nothing`)
.build(),
},
},
Expand Down Expand Up @@ -3242,9 +3241,8 @@ Resources:
verification: {
position: { line: 299, character: 42 },
expectation: HoverExpectationBuilder.create()
.expectStartsWith('**Resource:** LaunchTemplate')
.expectStartsWith('```typescript\n(resource) LaunchTemplate')
.expectContainsText(['LaunchTemplate', 'AWS::EC2::LaunchTemplate'])
.todo('Hover returns nothing')
.build(),
},
},
Expand All @@ -3269,8 +3267,7 @@ Resources:
position: { line: 303, character: 23 },
expectation: HoverExpectationBuilder.create()
.expectStartsWith('**Condition:** HasMultipleAZs')
.expectContainsText(['HasMultipleAZs', '!Not', '!Equals', '!Select'])
.todo('Hover returns nothing')
.expectContainsText(['HasMultipleAZs', 'Fn::Not', 'Fn::Equals', 'Fn::Select'])
.build(),
},
},
Expand Down
Loading