diff --git a/src/autocomplete/CompletionRouter.ts b/src/autocomplete/CompletionRouter.ts index 600af0c7..0de3666e 100644 --- a/src/autocomplete/CompletionRouter.ts +++ b/src/autocomplete/CompletionRouter.ts @@ -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 { diff --git a/src/autocomplete/IntrinsicFunctionArgumentCompletionProvider.ts b/src/autocomplete/IntrinsicFunctionArgumentCompletionProvider.ts index d476dd56..bd6cb0d0 100644 --- a/src/autocomplete/IntrinsicFunctionArgumentCompletionProvider.ts +++ b/src/autocomplete/IntrinsicFunctionArgumentCompletionProvider.ts @@ -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, @@ -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(); + 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, diff --git a/src/context/ContextType.ts b/src/context/ContextType.ts index 2862f7ce..ac0c9873 100644 --- a/src/context/ContextType.ts +++ b/src/context/ContextType.ts @@ -52,6 +52,7 @@ export enum IntrinsicShortForms { export enum EntitySection { Condition = 'Condition', + Properties = 'Properties', } export const IntrinsicsUsingConditionKeyword: ReadonlyArray = [ diff --git a/tst/integration/autocomplete/Autocomplete.test.ts b/tst/integration/autocomplete/Autocomplete.test.ts index b058c374..abeb3f41 100644 --- a/tst/integration/autocomplete/Autocomplete.test.ts +++ b/tst/integration/autocomplete/Autocomplete.test.ts @@ -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(), }, }, @@ -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(), }, }, ], @@ -3018,7 +3006,6 @@ Resources: 'IsProductionOrStaging', ]) .expectExcludesItems(['ComplexCondition', 'HasMultipleAZs']) - .todo('Not returning anything') .build(), }, }, diff --git a/tst/integration/hover/Hover.test.ts b/tst/integration/hover/Hover.test.ts index 37f16f80..55743e78 100644 --- a/tst/integration/hover/Hover.test.ts +++ b/tst/integration/hover/Hover.test.ts @@ -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(), }, }, @@ -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(), }, }, @@ -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(), }, },