diff --git a/src/context/Context.ts b/src/context/Context.ts index d2b9e8cc..164a8d7d 100644 --- a/src/context/Context.ts +++ b/src/context/Context.ts @@ -181,30 +181,31 @@ export class Context { return false; } - // Case 1: propertyPath.length === 3 (e.g., ['Resources', 'MyResource', 'Type']) - // We're at a resource attribute level - if (this.propertyPath.length === 3 && this.entitySection === this.text) { - return true; + // Case 1: If we are over 3 we know for sure we are beyond the entity level + if (this.propertyPath.length > 3) { + return false; } - // Case 2: propertyPath.length === 2 (e.g., ['Resources', 'MyResource']) - // We need to distinguish between: - // - Cursor in middle of resource name (should return false) - // - Cursor after resource name, ready for attributes (should return true) - if (this.propertyPath.length === 2) { - // If the current text matches the logical ID (resource name), - // it means the cursor is positioned within the resource name itself - // In this case, we should NOT provide entity key completions - if (this.text === this.logicalId) { + // Case 2: Two situations exist that we need to account for: + // isKey and isValue can be True when at the first key inside a value + // when we are at level 2 this means we are at Entity/LogicalId as the first key + // when we are at level 3 this means we are at Entity/LogicalId/Properties as the first key + if (this.isKey() && this.isValue()) { + if (this.propertyPath.length === 2) { + return true; + } else if (this.propertyPath.length === 3) { return false; } + } - // If entitySection is undefined and text is not the resource name, - // it means we're positioned after the resource name, ready for attributes - return this.entitySection !== this.text; + // Case 3 propertyPath.length === 2 (e.g., ['Resources', 'MyResource']) + // We need to see if the cursor is in the resource logical id + if (this.propertyPath.length === 2 && this.text === this.logicalId) { + return false; } - return false; + // Catch all at this point to say that the isKey is the most important thing + return this.isKey(); } public getMappingKeys(): string[] { diff --git a/tst/unit/autocomplete/EntityFieldCompletionProvider.test.ts b/tst/unit/autocomplete/EntityFieldCompletionProvider.test.ts index 8c385464..f2539277 100644 --- a/tst/unit/autocomplete/EntityFieldCompletionProvider.test.ts +++ b/tst/unit/autocomplete/EntityFieldCompletionProvider.test.ts @@ -14,7 +14,11 @@ describe('EntityFieldCompletionProvider', () => { describe('Parameter', () => { test('should suggest Default, Description, ConstraintDescription with e as partial string', () => { - const mockContext = createParameterContext('MyParameter', { text: 'e', data: { Type: 'String' } }); + const mockContext = createParameterContext('MyParameter', { + text: 'e', + data: { Type: 'String' }, + propertyPath: ['Parameters', 'MyParameter', 'e'], + }); const result = parameterFieldCompletionProvider.getCompletions(mockContext, mockParams); expect(result).toBeDefined(); expect(result?.length).equal(9); @@ -23,7 +27,11 @@ describe('EntityFieldCompletionProvider', () => { }); test('should be robust against typos and suggest Type when partial string is yp', () => { - const mockContext = createParameterContext('MyParameter', { text: 'yp', data: { Type: undefined } }); + const mockContext = createParameterContext('MyParameter', { + text: 'yp', + data: { Type: undefined }, + propertyPath: ['Parameters', 'MyParameter', 'yp'], + }); const result = parameterFieldCompletionProvider.getCompletions(mockContext, mockParams); expect(result).toBeDefined(); expect(result?.length).equal(1); @@ -37,6 +45,7 @@ describe('EntityFieldCompletionProvider', () => { Type: 'string', Description: 'some description', }, + propertyPath: ['Parameters', 'MyParameter', 'e'], }); const result = parameterFieldCompletionProvider.getCompletions(mockContext, mockParams); expect(result).toBeDefined(); @@ -47,7 +56,11 @@ describe('EntityFieldCompletionProvider', () => { }); test('should suggest all available fields starting with Type (required) when nothing typed yet', () => { - const mockContext = createParameterContext('MyParameter', { text: '', data: { Type: undefined } }); + const mockContext = createParameterContext('MyParameter', { + text: '', + data: { Type: undefined }, + propertyPath: ['Parameters', 'MyParameter', ''], + }); const result = parameterFieldCompletionProvider.getCompletions(mockContext, mockParams); expect(result).toBeDefined(); // All Parameter fields should be suggested when none are defined @@ -80,6 +93,7 @@ describe('EntityFieldCompletionProvider', () => { Description: 'some description', Default: 'default value', }, + propertyPath: ['Parameters', 'MyParameter', ''], }); const result = parameterFieldCompletionProvider.getCompletions(mockContext, mockParams); expect(result).toBeDefined(); @@ -103,7 +117,10 @@ describe('EntityFieldCompletionProvider', () => { describe('Output', () => { test('should suggest export and description with e as partial string', () => { - const mockContext = createOutputContext('MyOutput', { text: 'e' }); + const mockContext = createOutputContext('MyOutput', { + text: 'e', + propertyPath: ['Outputs', 'MyOutput', 'e'], + }); const result = outputFieldCompletionProvider.getCompletions(mockContext, mockParams); expect(result).toBeDefined(); expect(result?.length).equal(2); @@ -112,7 +129,10 @@ describe('EntityFieldCompletionProvider', () => { }); test('should be robust against typos and suggest Export when partial string is xpo', () => { - const mockContext = createOutputContext('MyOutput', { text: 'xpo' }); + const mockContext = createOutputContext('MyOutput', { + text: 'xpo', + propertyPath: ['Outputs', 'MyOutput', 'xpo'], + }); const result = outputFieldCompletionProvider.getCompletions(mockContext, mockParams); expect(result).toBeDefined(); expect(result?.length).equal(1); @@ -125,6 +145,7 @@ describe('EntityFieldCompletionProvider', () => { data: { Description: 'some description', }, + propertyPath: ['Outputs', 'MyOutput', 'e'], }); const result = outputFieldCompletionProvider.getCompletions(mockContext, mockParams); expect(result).toBeDefined(); diff --git a/tst/unit/autocomplete/ResourceEntityCompletionProvider.test.ts b/tst/unit/autocomplete/ResourceEntityCompletionProvider.test.ts index ee13162f..dd23629b 100644 --- a/tst/unit/autocomplete/ResourceEntityCompletionProvider.test.ts +++ b/tst/unit/autocomplete/ResourceEntityCompletionProvider.test.ts @@ -2,6 +2,7 @@ import { describe, expect, test, beforeEach } from 'vitest'; import { CompletionParams, CompletionItemKind, CompletionItem, InsertTextFormat } from 'vscode-languageserver'; import { ResourceEntityCompletionProvider } from '../../../src/autocomplete/ResourceEntityCompletionProvider'; import { ResourceAttribute } from '../../../src/context/ContextType'; +import { YamlNodeTypes } from '../../../src/context/syntaxtree/utils/TreeSitterTypes'; import { CombinedSchemas } from '../../../src/schema/CombinedSchemas'; import { ResourceSchema } from '../../../src/schema/ResourceSchema'; import { ExtensionName } from '../../../src/utils/ExtensionConfig'; @@ -23,7 +24,11 @@ describe('ResourceEntityCompletionProvider', () => { }); test('should return resource heading completions when inside Resources section but not inside a resource type', () => { - const mockContext = createResourceContext('MyResource', { text: '' }); + const mockContext = createResourceContext('MyResource', { + text: '', + nodeType: YamlNodeTypes.STRING_SCALAR, + propertyPath: ['Resources', 'MyResource', ''], + }); const result = provider.getCompletions(mockContext, mockParams); @@ -58,7 +63,10 @@ describe('ResourceEntityCompletionProvider', () => { }); test('should return filtered resource heading completions when text is provided', () => { - const mockContext = createResourceContext('MyResource', { text: 'Prop' }); + const mockContext = createResourceContext('MyResource', { + text: '', + propertyPath: ['Resources', 'MyResource', ''], + }); const result = provider.getCompletions(mockContext, mockParams); @@ -72,7 +80,10 @@ describe('ResourceEntityCompletionProvider', () => { }); test('should provide correct insert text for resource properties', () => { - const mockContext = createResourceContext('MyResource', { text: '' }); + const mockContext = createResourceContext('MyResource', { + text: '', + propertyPath: ['Resources', 'MyResource', ''], + }); const completions = provider.getCompletions(mockContext, mockParams); expect(completions).toBeDefined(); @@ -137,7 +148,7 @@ describe('ResourceEntityCompletionProvider', () => { // Setup context with a resource that has a Type const mockContext = createResourceContext('MyInstance', { text: '', - propertyPath: ['Resources', 'MyInstance'], + propertyPath: ['Resources', 'MyInstance', ''], data: { Type: 'AWS::EC2::Instance', }, @@ -166,7 +177,7 @@ describe('ResourceEntityCompletionProvider', () => { // Setup context with a resource that has a Type const mockContext = createResourceContext('MyInstance', { text: '', - propertyPath: ['Resources', 'MyInstance'], + propertyPath: ['Resources', 'MyInstance', ''], data: { Type: 'AWS::EC2::Instance', }, @@ -193,7 +204,7 @@ describe('ResourceEntityCompletionProvider', () => { // Setup context with a resource that has a Type const mockContext = createResourceContext('MyInstance', { text: '', - propertyPath: ['Resources', 'MyInstance'], + propertyPath: ['Resources', 'MyInstance', ''], data: { Type: 'AWS::EC2::Instance', }, @@ -218,7 +229,7 @@ describe('ResourceEntityCompletionProvider', () => { // Setup context with a resource that has no Type const mockContext = createResourceContext('MyResource', { text: '', - propertyPath: ['Resources', 'MyResource'], + propertyPath: ['Resources', 'MyResource', ''], data: {}, }); @@ -238,7 +249,7 @@ describe('ResourceEntityCompletionProvider', () => { // Setup context with a resource that has a Type const mockContext = createResourceContext('MyResource', { text: '', - propertyPath: ['Resources', 'MyResource'], + propertyPath: ['Resources', 'MyResource', ''], data: { Type: 'AWS::Unknown::Resource', }, diff --git a/tst/unit/autocomplete/ResourceSectionCompletionProvider.test.ts b/tst/unit/autocomplete/ResourceSectionCompletionProvider.test.ts index 528430c6..991898f6 100644 --- a/tst/unit/autocomplete/ResourceSectionCompletionProvider.test.ts +++ b/tst/unit/autocomplete/ResourceSectionCompletionProvider.test.ts @@ -108,7 +108,10 @@ describe('ResourceSectionCompletionProvider', () => { }); test('should delegate to entity provider when at entity key level', async () => { - const mockContext = createResourceContext('MyResource', { text: '' }); + const mockContext = createResourceContext('MyResource', { + text: '', + propertyPath: ['Resources', 'MyResource', ''], + }); const entityProvider = resourceProviders.get('Entity' as any)!; const mockCompletions = [ { label: 'Type', kind: CompletionItemKind.Property }, @@ -147,7 +150,7 @@ describe('ResourceSectionCompletionProvider', () => { test('should delegate to property provider when at nested entity key level Properties', async () => { const mockContext = createResourceContext('MyBucket', { text: 'Bucket', - propertyPath: ['Resources', 'MyBucket', 'Properties'], + propertyPath: ['Resources', 'MyBucket', 'Properties', 'Bucket'], data: { Type: 'AWS::S3::Bucket', Properties: {},