diff --git a/src/autocomplete/InlineCompletionRouter.ts b/src/autocomplete/InlineCompletionRouter.ts index b6593f3a..fa659bbd 100644 --- a/src/autocomplete/InlineCompletionRouter.ts +++ b/src/autocomplete/InlineCompletionRouter.ts @@ -1,6 +1,9 @@ import { InlineCompletionList, InlineCompletionParams, InlineCompletionItem } from 'vscode-languageserver-protocol'; +import { Context } from '../context/Context'; import { ContextManager } from '../context/ContextManager'; +import { DocumentManager } from '../document/DocumentManager'; import { Closeable, Configurable, ServerComponents } from '../server/ServerComponents'; +import { RelationshipSchemaService } from '../services/RelationshipSchemaService'; import { CompletionSettings, DefaultSettings, @@ -9,8 +12,10 @@ import { SettingsSubscription, } from '../settings/Settings'; import { LoggerFactory } from '../telemetry/LoggerFactory'; +import { InlineCompletionProvider } from './InlineCompletionProvider'; +import { RelatedResourcesInlineCompletionProvider } from './RelatedResourcesInlineCompletionProvider'; -export type InlineCompletionProviderType = 'ResourceBlock' | 'PropertyBlock' | 'TemplateSection' | 'AIGenerated'; +export type InlineCompletionProviderType = 'RelatedResources' | 'ResourceBlock' | 'PropertyBlock'; type ReturnType = InlineCompletionList | InlineCompletionItem[] | null | undefined; export class InlineCompletionRouter implements Configurable, Closeable { @@ -20,7 +25,10 @@ export class InlineCompletionRouter implements Configurable, Closeable { private editorSettingsSubscription?: SettingsSubscription; private readonly log = LoggerFactory.getLogger(InlineCompletionRouter); - constructor(private readonly contextManager: ContextManager) {} + constructor( + private readonly contextManager: ContextManager, + private readonly inlineCompletionProviderMap: Map, + ) {} getInlineCompletions(params: InlineCompletionParams): Promise | ReturnType { if (!this.completionSettings.enabled) return; @@ -31,6 +39,31 @@ export class InlineCompletionRouter implements Configurable, Closeable { return; } + this.log.debug( + { + position: params.position, + section: context.section, + propertyPath: context.propertyPath, + }, + 'Processing inline completion request', + ); + + // Check if we are authoring a new resource + if (this.isAuthoringNewResource(context)) { + const relatedResourcesProvider = this.inlineCompletionProviderMap.get('RelatedResources'); + if (relatedResourcesProvider) { + const result = relatedResourcesProvider.getlineCompletion(context, params, this.editorSettings); + + if (result instanceof Promise) { + return result.then((items) => { + return { items }; + }); + } else if (result && Array.isArray(result)) { + return { items: result }; + } + } + } + return; } @@ -42,18 +75,14 @@ export class InlineCompletionRouter implements Configurable, Closeable { this.editorSettingsSubscription.unsubscribe(); } - // Get initial settings - this.completionSettings = settingsManager.getCurrentSettings().completion; - this.editorSettings = settingsManager.getCurrentSettings().editor; - // Subscribe to completion settings changes this.settingsSubscription = settingsManager.subscribe('completion', (newCompletionSettings) => { - this.onCompletionSettingsChanged(newCompletionSettings); + this.completionSettings = newCompletionSettings; }); // Subscribe to editor settings changes this.editorSettingsSubscription = settingsManager.subscribe('editor', (newEditorSettings) => { - this.onEditorSettingsChanged(newEditorSettings); + this.editorSettings = newEditorSettings; }); } @@ -68,15 +97,36 @@ export class InlineCompletionRouter implements Configurable, Closeable { } } - private onCompletionSettingsChanged(settings: CompletionSettings): void { - this.completionSettings = settings; - } - - private onEditorSettingsChanged(settings: EditorSettings): void { - this.editorSettings = settings; + private isAuthoringNewResource(context: Context): boolean { + // Only provide suggestions in Resources section when positioned for a new resource + return ( + String(context.section) === 'Resources' && + (context.propertyPath.length === 1 || + (context.propertyPath.length === 2 && + context.hasLogicalId && + context.isKey() && + !context.atEntityKeyLevel())) + ); } static create(components: ServerComponents) { - return new InlineCompletionRouter(components.contextManager); + return new InlineCompletionRouter( + components.contextManager, + createInlineCompletionProviders(components.documentManager, RelationshipSchemaService.getInstance()), + ); } } + +export function createInlineCompletionProviders( + documentManager: DocumentManager, + relationshipSchemaService: RelationshipSchemaService, +): Map { + const inlineCompletionProviderMap = new Map(); + + inlineCompletionProviderMap.set( + 'RelatedResources', + new RelatedResourcesInlineCompletionProvider(relationshipSchemaService, documentManager), + ); + + return inlineCompletionProviderMap; +} diff --git a/src/autocomplete/RelatedResourcesInlineCompletionProvider.ts b/src/autocomplete/RelatedResourcesInlineCompletionProvider.ts new file mode 100644 index 00000000..9299c0b8 --- /dev/null +++ b/src/autocomplete/RelatedResourcesInlineCompletionProvider.ts @@ -0,0 +1,141 @@ +import { InlineCompletionItem, InlineCompletionParams } from 'vscode-languageserver-protocol'; +import { Context } from '../context/Context'; +import { DocumentManager } from '../document/DocumentManager'; +import { RelationshipSchemaService } from '../services/RelationshipSchemaService'; +import { EditorSettings } from '../settings/Settings'; +import { LoggerFactory } from '../telemetry/LoggerFactory'; +import { InlineCompletionProvider } from './InlineCompletionProvider'; + +export class RelatedResourcesInlineCompletionProvider implements InlineCompletionProvider { + private readonly log = LoggerFactory.getLogger(RelatedResourcesInlineCompletionProvider); + + constructor( + private readonly relationshipSchemaService: RelationshipSchemaService, + private readonly documentManager: DocumentManager, + ) {} + + getlineCompletion( + context: Context, + params: InlineCompletionParams, + _editorSettings: EditorSettings, + ): Promise | InlineCompletionItem[] | undefined { + this.log.debug( + { + provider: 'RelatedResourcesInlineCompletion', + position: params.position, + section: context.section, + propertyPath: context.propertyPath, + }, + 'Processing related resources inline completion request', + ); + + try { + const document = this.documentManager.get(params.textDocument.uri); + if (!document) { + return undefined; + } + + const existingResourceTypes = this.relationshipSchemaService.extractResourceTypesFromTemplate( + document.getText(), + ); + + if (existingResourceTypes.length === 0) { + return undefined; + } + + const relatedResourceTypes = this.getRelatedResourceTypes(existingResourceTypes); + + if (relatedResourceTypes.length === 0) { + return undefined; + } + + return this.generateInlineCompletionItems(relatedResourceTypes, params); + } catch (error) { + this.log.error({ error: String(error) }, 'Error generating related resources inline completion'); + return undefined; + } + } + + private getRelatedResourceTypes(existingResourceTypes: string[]): string[] { + const existingRelationships = new Map>(); + const allRelatedTypes = new Set(); + + for (const resourceType of existingResourceTypes) { + const relatedTypes = this.relationshipSchemaService.getAllRelatedResourceTypes(resourceType); + existingRelationships.set(resourceType, relatedTypes); + for (const type of relatedTypes) { + allRelatedTypes.add(type); + } + } + + const existingTypesSet = new Set(existingResourceTypes); + const suggestedTypes = [...allRelatedTypes].filter((type) => !existingTypesSet.has(type)); + + return this.rankSuggestionsByFrequency(suggestedTypes, existingRelationships); + } + + private rankSuggestionsByFrequency( + suggestedTypes: string[], + existingRelationships: Map>, + ): string[] { + const frequencyMap = new Map(); + + for (const suggestedType of suggestedTypes) { + let frequency = 0; + for (const relatedTypes of existingRelationships.values()) { + if (relatedTypes.has(suggestedType)) { + frequency++; + } + } + frequencyMap.set(suggestedType, frequency); + } + + return suggestedTypes.sort((a, b) => { + const freqA = frequencyMap.get(a) ?? 0; + const freqB = frequencyMap.get(b) ?? 0; + + if (freqA !== freqB) { + return freqB - freqA; + } + + return a.localeCompare(b); + }); + } + + private generateInlineCompletionItems( + relatedResourceTypes: string[], + params: InlineCompletionParams, + ): InlineCompletionItem[] { + const completionItems: InlineCompletionItem[] = []; + + const topSuggestions = relatedResourceTypes.slice(0, 5); + + for (const resourceType of topSuggestions) { + const insertText = this.generatePropertySnippet(resourceType); + + completionItems.push({ + insertText, + range: { + start: params.position, + end: params.position, + }, + filterText: `${resourceType}`, + }); + } + + this.log.debug( + { + suggestedCount: completionItems.length, + suggestions: completionItems.map((item) => item.insertText), + }, + 'Generated related resource inline completions', + ); + + return completionItems; + } + + private generatePropertySnippet(resourceType: string): string { + // TODO: Convert AWS::Service::Resource to a complete resource type snippet + return `${resourceType}:`; + } +} diff --git a/tst/unit/autocomplete/InlineCompletionRouter.test.ts b/tst/unit/autocomplete/InlineCompletionRouter.test.ts index f6f9ac94..9f1d7fee 100644 --- a/tst/unit/autocomplete/InlineCompletionRouter.test.ts +++ b/tst/unit/autocomplete/InlineCompletionRouter.test.ts @@ -1,14 +1,24 @@ import { describe, expect, test, beforeEach, vi } from 'vitest'; import { InlineCompletionParams, InlineCompletionTriggerKind } from 'vscode-languageserver-protocol'; -import { InlineCompletionRouter } from '../../../src/autocomplete/InlineCompletionRouter'; +import { + InlineCompletionRouter, + createInlineCompletionProviders, +} from '../../../src/autocomplete/InlineCompletionRouter'; import { DocumentType } from '../../../src/document/Document'; +import { RelationshipSchemaService } from '../../../src/services/RelationshipSchemaService'; import { DefaultSettings } from '../../../src/settings/Settings'; import { createTopLevelContext } from '../../utils/MockContext'; -import { createMockContextManager, createMockSettingsManager } from '../../utils/MockServerComponents'; +import { + createMockContextManager, + createMockDocumentManager, + createMockSettingsManager, +} from '../../utils/MockServerComponents'; describe('InlineCompletionRouter', () => { const mockContextManager = createMockContextManager(); + const mockDocumentManager = createMockDocumentManager(); const mockSettingsManager = createMockSettingsManager(); + const mockRelationshipSchemaService = RelationshipSchemaService.getInstance(); let router: InlineCompletionRouter; const mockParams: InlineCompletionParams = { @@ -21,7 +31,8 @@ describe('InlineCompletionRouter', () => { beforeEach(() => { mockContextManager.getContext.reset(); - router = new InlineCompletionRouter(mockContextManager); + const providers = createInlineCompletionProviders(mockDocumentManager, mockRelationshipSchemaService); + router = new InlineCompletionRouter(mockContextManager, providers); router.configure(mockSettingsManager); vi.restoreAllMocks(); }); @@ -32,6 +43,13 @@ describe('InlineCompletionRouter', () => { completion: { ...DefaultSettings.completion, enabled: false }, }; const disabledSettingsManager = createMockSettingsManager(disabledSettings); + disabledSettingsManager.subscribe.callsFake((path: keyof typeof disabledSettings, callback: any) => { + callback(disabledSettings[path]); + return { + unsubscribe: () => {}, + isActive: () => true, + }; + }); router.configure(disabledSettingsManager); const result = router.getInlineCompletions(mockParams); @@ -51,7 +69,7 @@ describe('InlineCompletionRouter', () => { }); test('should return undefined when context exists but no providers match', () => { - const mockContext = createTopLevelContext('Resources', { text: 'AWS::' }); + const mockContext = createTopLevelContext('Parameters', { text: 'AWS::' }); mockContextManager.getContext.returns(mockContext); const result = router.getInlineCompletions(mockParams); @@ -125,26 +143,6 @@ describe('InlineCompletionRouter', () => { }); describe('Settings Management', () => { - test('should update completion settings when notified', () => { - const newCompletionSettings = { ...DefaultSettings.completion, enabled: false }; - - // Simulate settings change - router['onCompletionSettingsChanged'](newCompletionSettings); - - // Verify settings were updated - expect(router['completionSettings']).toEqual(newCompletionSettings); - }); - - test('should update editor settings when notified', () => { - const newEditorSettings = { ...DefaultSettings.editor, tabSize: 8 }; - - // Simulate settings change - router['onEditorSettingsChanged'](newEditorSettings); - - // Verify settings were updated - expect(router['editorSettings']).toEqual(newEditorSettings); - }); - test('should configure with settings manager', () => { const customSettings = { ...DefaultSettings, @@ -152,6 +150,13 @@ describe('InlineCompletionRouter', () => { editor: { ...DefaultSettings.editor, tabSize: 8 }, }; const customSettingsManager = createMockSettingsManager(customSettings); + customSettingsManager.subscribe.callsFake((path: keyof typeof customSettings, callback: any) => { + callback(customSettings[path]); + return { + unsubscribe: () => {}, + isActive: () => true, + }; + }); router.configure(customSettingsManager); @@ -172,7 +177,21 @@ describe('InlineCompletionRouter', () => { }; const firstSettingsManager = createMockSettingsManager(firstSettings); + firstSettingsManager.subscribe.callsFake((path: keyof typeof firstSettings, callback: any) => { + callback(firstSettings[path]); + return { + unsubscribe: () => {}, + isActive: () => true, + }; + }); const secondSettingsManager = createMockSettingsManager(secondSettings); + secondSettingsManager.subscribe.callsFake((path: keyof typeof secondSettings, callback: any) => { + callback(secondSettings[path]); + return { + unsubscribe: () => {}, + isActive: () => true, + }; + }); // Configure with first settings manager router.configure(firstSettingsManager); @@ -196,6 +215,13 @@ describe('InlineCompletionRouter', () => { editor: { ...DefaultSettings.editor, tabSize: 6 }, }; const customSettingsManager = createMockSettingsManager(customSettings); + customSettingsManager.subscribe.callsFake((path: keyof typeof customSettings, callback: any) => { + callback(customSettings[path]); + return { + unsubscribe: () => {}, + isActive: () => true, + }; + }); router.configure(customSettingsManager); @@ -216,10 +242,67 @@ describe('InlineCompletionRouter', () => { }); }); + describe('Related Resources Provider', () => { + test('should attempt to use related resources provider for Resources section at top level', () => { + const mockContext = createTopLevelContext('Resources', { + text: '', + propertyPath: ['Resources'], + }); + mockContextManager.getContext.returns(mockContext); + + const result = router.getInlineCompletions(mockParams); + + expect(mockContextManager.getContext.calledOnce).toBe(true); + // Should attempt to use related resources provider but return undefined due to no existing resources + expect(result).toBeUndefined(); + }); + + test('should not use related resources provider for non-Resources section', () => { + const mockContext = createTopLevelContext('Parameters', { + text: '', + propertyPath: ['Parameters'], + }); + mockContextManager.getContext.returns(mockContext); + + const result = router.getInlineCompletions(mockParams); + + expect(result).toBeUndefined(); + expect(mockContextManager.getContext.calledOnce).toBe(true); + }); + + test('should handle Resources section with resource-level context', () => { + const mockContext = createTopLevelContext('Resources', { + text: 'MyResource', + propertyPath: ['Resources', 'MyResource'], + }); + mockContextManager.getContext.returns(mockContext); + + const result = router.getInlineCompletions(mockParams); + + expect(mockContextManager.getContext.calledOnce).toBe(true); + // Should return undefined since no existing resources to suggest from + expect(result).toBeUndefined(); + }); + + test('should not use related resources provider for deep property paths', () => { + const mockContext = createTopLevelContext('Resources', { + text: 'BucketName', + propertyPath: ['Resources', 'MyBucket', 'Properties', 'BucketName'], + }); + mockContextManager.getContext.returns(mockContext); + + const result = router.getInlineCompletions(mockParams); + + expect(result).toBeUndefined(); + expect(mockContextManager.getContext.calledOnce).toBe(true); + }); + }); + describe('Static Factory Method', () => { test('should create router with components', () => { const mockComponents = { contextManager: mockContextManager, + documentManager: mockDocumentManager, } as any; const createdRouter = InlineCompletionRouter.create(mockComponents); diff --git a/tst/unit/autocomplete/RelatedResourcesInlineCompletionProvider.test.ts b/tst/unit/autocomplete/RelatedResourcesInlineCompletionProvider.test.ts new file mode 100644 index 00000000..3a7b018f --- /dev/null +++ b/tst/unit/autocomplete/RelatedResourcesInlineCompletionProvider.test.ts @@ -0,0 +1,255 @@ +import { describe, expect, test, beforeEach, vi } from 'vitest'; +import { InlineCompletionParams, InlineCompletionTriggerKind } from 'vscode-languageserver-protocol'; +import { RelatedResourcesInlineCompletionProvider } from '../../../src/autocomplete/RelatedResourcesInlineCompletionProvider'; +import { RelationshipSchemaService } from '../../../src/services/RelationshipSchemaService'; +import { DefaultSettings } from '../../../src/settings/Settings'; +import { createTopLevelContext } from '../../utils/MockContext'; +import { createMockDocumentManager } from '../../utils/MockServerComponents'; + +describe('RelatedResourcesInlineCompletionProvider', () => { + const mockDocumentManager = createMockDocumentManager(); + const mockRelationshipSchemaService = RelationshipSchemaService.getInstance(); + let provider: RelatedResourcesInlineCompletionProvider; + + const mockParams: InlineCompletionParams = { + textDocument: { uri: 'file:///test.yaml' }, + position: { line: 5, character: 0 }, + context: { + triggerKind: InlineCompletionTriggerKind.Invoked, + }, + }; + + const mockEditorSettings = DefaultSettings.editor; + + beforeEach(() => { + mockDocumentManager.get.reset(); + provider = new RelatedResourcesInlineCompletionProvider(mockRelationshipSchemaService, mockDocumentManager); + vi.restoreAllMocks(); + }); + + describe('getlineCompletion', () => { + test('should return undefined when document is not found', () => { + const mockContext = createTopLevelContext('Resources', { + text: '', + propertyPath: ['Resources'], + }); + mockDocumentManager.get.returns(undefined); + + const result = provider.getlineCompletion(mockContext, mockParams, mockEditorSettings); + + expect(result).toBeUndefined(); + expect(mockDocumentManager.get.calledOnce).toBe(true); + expect(mockDocumentManager.get.calledWith(mockParams.textDocument.uri)).toBe(true); + }); + + test('should return undefined when no existing resources are found', () => { + const mockContext = createTopLevelContext('Resources', { + text: '', + propertyPath: ['Resources'], + }); + const mockDocument = { + getText: () => 'AWSTemplateFormatVersion: "2010-09-09"\nResources:\n', + }; + mockDocumentManager.get.returns(mockDocument as any); + + // Mock the service to return empty array for no resources + vi.spyOn(mockRelationshipSchemaService, 'extractResourceTypesFromTemplate').mockReturnValue([]); + + const result = provider.getlineCompletion(mockContext, mockParams, mockEditorSettings); + + expect(result).toBeUndefined(); + }); + + test('should return undefined when no related resources are found', () => { + const mockContext = createTopLevelContext('Resources', { + text: '', + propertyPath: ['Resources'], + }); + const mockDocument = { + getText: () => ` +AWSTemplateFormatVersion: "2010-09-09" +Resources: + MyBucket: + Type: AWS::S3::Bucket +`, + }; + mockDocumentManager.get.returns(mockDocument as any); + + // Mock the service to return existing resources but no related ones + vi.spyOn(mockRelationshipSchemaService, 'extractResourceTypesFromTemplate').mockReturnValue([ + 'AWS::S3::Bucket', + ]); + vi.spyOn(mockRelationshipSchemaService, 'getAllRelatedResourceTypes').mockReturnValue(new Set()); + + const result = provider.getlineCompletion(mockContext, mockParams, mockEditorSettings); + + expect(result).toBeUndefined(); + }); + + test('should return completion items when related resources exist', () => { + const mockContext = createTopLevelContext('Resources', { + text: '', + propertyPath: ['Resources'], + }); + const mockDocument = { + getText: () => ` +AWSTemplateFormatVersion: "2010-09-09" +Resources: + MyBucket: + Type: AWS::S3::Bucket +`, + }; + mockDocumentManager.get.returns(mockDocument as any); + + // Mock the service to return existing resources and related ones + vi.spyOn(mockRelationshipSchemaService, 'extractResourceTypesFromTemplate').mockReturnValue([ + 'AWS::S3::Bucket', + ]); + vi.spyOn(mockRelationshipSchemaService, 'getAllRelatedResourceTypes').mockReturnValue( + new Set(['AWS::Lambda::Function', 'AWS::IAM::Role', 'AWS::CloudFront::Distribution']), + ); + + const result = provider.getlineCompletion(mockContext, mockParams, mockEditorSettings) as any[]; + + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBeGreaterThan(0); + expect(result.length).toBeLessThanOrEqual(5); // Should limit to top 5 + + // Verify actual completion item data + const expectedResourceTypes = ['AWS::Lambda::Function', 'AWS::IAM::Role', 'AWS::CloudFront::Distribution']; + const actualFilterTexts = result.map((item) => item.filterText); + + // Verify that suggested resources are from the expected set + for (const item of result) { + expect(item.insertText).toBeDefined(); + expect(item.insertText).toMatch(/^AWS::[A-Za-z0-9]+::[A-Za-z0-9]+:$/); // Should end with colon + expect(item.range).toBeDefined(); + expect(item.range.start).toEqual(mockParams.position); + expect(item.range.end).toEqual(mockParams.position); + expect(item.filterText).toBeDefined(); + + // Verify the resource type is one of the expected ones + const resourceType = item.filterText; + expect(expectedResourceTypes).toContain(resourceType); + expect(item.insertText).toBe(`${resourceType}:`); + } + + // Verify no existing resources are suggested + expect(actualFilterTexts).not.toContain('AWS::S3::Bucket'); + }); + + test('should handle errors gracefully', () => { + const mockContext = createTopLevelContext('Resources', { + text: '', + propertyPath: ['Resources'], + }); + mockDocumentManager.get.throws(new Error('Document access error')); + + const result = provider.getlineCompletion(mockContext, mockParams, mockEditorSettings); + + expect(result).toBeUndefined(); + }); + }); + + describe('getRelatedResourceTypes', () => { + test('should filter out existing resource types from suggestions', () => { + const existingTypes = ['AWS::S3::Bucket', 'AWS::Lambda::Function']; + + // Mock to return related types that include some existing ones + vi.spyOn(mockRelationshipSchemaService, 'getAllRelatedResourceTypes') + .mockReturnValueOnce(new Set(['AWS::IAM::Role', 'AWS::Lambda::Function'])) // For S3 + .mockReturnValueOnce(new Set(['AWS::IAM::Role', 'AWS::S3::Bucket'])); // For Lambda + + const result = (provider as any).getRelatedResourceTypes(existingTypes); + + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + // Should only include AWS::IAM::Role (not the existing S3 and Lambda) + expect(result).toEqual(['AWS::IAM::Role']); + }); + + test('should rank suggestions by frequency', () => { + const existingTypes = ['AWS::S3::Bucket', 'AWS::Lambda::Function', 'AWS::EC2::Instance']; + + // Mock to return overlapping related types + vi.spyOn(mockRelationshipSchemaService, 'getAllRelatedResourceTypes') + .mockReturnValueOnce(new Set(['AWS::IAM::Role', 'AWS::CloudFront::Distribution'])) // For S3 + .mockReturnValueOnce(new Set(['AWS::IAM::Role', 'AWS::API::Gateway'])) // For Lambda + .mockReturnValueOnce(new Set(['AWS::IAM::Role', 'AWS::EC2::SecurityGroup'])); // For EC2 + + const result = (provider as any).getRelatedResourceTypes(existingTypes); + + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + // AWS::IAM::Role should be first (appears in all 3), others alphabetically + expect(result[0]).toBe('AWS::IAM::Role'); + }); + }); + + describe('generateInlineCompletionItems', () => { + test('should limit suggestions to top 5', () => { + const manyResourceTypes = [ + 'AWS::IAM::Role', + 'AWS::CloudFront::Distribution', + 'AWS::API::Gateway', + 'AWS::EC2::SecurityGroup', + 'AWS::RDS::DBInstance', + 'AWS::DynamoDB::Table', + 'AWS::SNS::Topic', + 'AWS::SQS::Queue', + ]; + + const result = (provider as any).generateInlineCompletionItems(manyResourceTypes, mockParams); + + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBe(5); // Should limit to 5 + }); + + test('should create proper completion items structure', () => { + const resourceTypes = ['AWS::IAM::Role', 'AWS::CloudFront::Distribution']; + + const result = (provider as any).generateInlineCompletionItems(resourceTypes, mockParams); + + expect(result).toBeDefined(); + expect(result.length).toBe(2); + + for (const item of result) { + expect(item.insertText).toBeDefined(); + expect(item.insertText).toContain(':'); + expect(item.range).toBeDefined(); + expect(item.range.start).toEqual(mockParams.position); + expect(item.range.end).toEqual(mockParams.position); + expect(item.filterText).toBeDefined(); + } + }); + + test('should handle empty resource types array', () => { + const result = (provider as any).generateInlineCompletionItems([], mockParams); + + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBe(0); + }); + }); + + describe('generatePropertySnippet', () => { + test('should generate basic property snippet', () => { + const resourceType = 'AWS::S3::Bucket'; + + const result = (provider as any).generatePropertySnippet(resourceType); + + expect(result).toBe('AWS::S3::Bucket:'); + }); + + test('should handle different resource types', () => { + const testCases = ['AWS::Lambda::Function', 'AWS::IAM::Role', 'AWS::EC2::Instance', 'AWS::RDS::DBInstance']; + + for (const resourceType of testCases) { + const result = (provider as any).generatePropertySnippet(resourceType); + expect(result).toBe(`${resourceType}:`); + } + }); + }); +});