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
35 changes: 18 additions & 17 deletions src/context/Context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] {
Expand Down
31 changes: 26 additions & 5 deletions tst/unit/autocomplete/EntityFieldCompletionProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -37,6 +45,7 @@ describe('EntityFieldCompletionProvider', () => {
Type: 'string',
Description: 'some description',
},
propertyPath: ['Parameters', 'MyParameter', 'e'],
});
const result = parameterFieldCompletionProvider.getCompletions(mockContext, mockParams);
expect(result).toBeDefined();
Expand All @@ -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
Expand Down Expand Up @@ -80,6 +93,7 @@ describe('EntityFieldCompletionProvider', () => {
Description: 'some description',
Default: 'default value',
},
propertyPath: ['Parameters', 'MyParameter', ''],
});
const result = parameterFieldCompletionProvider.getCompletions(mockContext, mockParams);
expect(result).toBeDefined();
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -125,6 +145,7 @@ describe('EntityFieldCompletionProvider', () => {
data: {
Description: 'some description',
},
propertyPath: ['Outputs', 'MyOutput', 'e'],
});
const result = outputFieldCompletionProvider.getCompletions(mockContext, mockParams);
expect(result).toBeDefined();
Expand Down
27 changes: 19 additions & 8 deletions tst/unit/autocomplete/ResourceEntityCompletionProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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);

Expand Down Expand Up @@ -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);

Expand All @@ -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();
Expand Down Expand Up @@ -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',
},
Expand Down Expand Up @@ -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',
},
Expand All @@ -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',
},
Expand All @@ -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: {},
});

Expand All @@ -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',
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down Expand Up @@ -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: {},
Expand Down
Loading