diff --git a/src/autocomplete/CompletionUtils.ts b/src/autocomplete/CompletionUtils.ts index 8a0f4356..effaaf78 100644 --- a/src/autocomplete/CompletionUtils.ts +++ b/src/autocomplete/CompletionUtils.ts @@ -4,6 +4,8 @@ import { CompletionParams, InsertTextFormat, InsertTextMode, + MarkupContent, + MarkupKind, Position, Range, TextEdit, @@ -33,6 +35,20 @@ export function createReplacementRange(context: Context, includeQuotes?: boolean } } +/** + * Creates a MarkupContent object from a markdown string. + * This ensures consistent formatting for completion documentation. + * + * @param markdown The markdown content string + * @returns A MarkupContent object with markdown formatting + */ +export function createMarkupContent(markdown: string): MarkupContent { + return { + kind: MarkupKind.Markdown, + value: markdown, + }; +} + /** * Creates a base completion item with common properties. * This reduces duplication across different completion providers. @@ -51,7 +67,7 @@ export function createCompletionItem( insertTextFormat?: InsertTextFormat; insertTextMode?: InsertTextMode; sortText?: string; - documentation?: string; + documentation?: string | MarkupContent; data?: Record; context?: Context; }, @@ -70,6 +86,23 @@ export function createCompletionItem( } } + // Handle documentation - support both string and MarkupContent + let documentation: string | MarkupContent | undefined; + if (options?.documentation) { + if (typeof options.documentation === 'string') { + // For string documentation, add the source attribution + documentation = `${options.documentation}\n\nSource: ${ExtensionName}`; + } else { + // For MarkupContent, add source attribution to the markdown value + documentation = { + kind: options.documentation.kind, + value: `${options.documentation.value}\n\n**Source:** ${ExtensionName}`, + }; + } + } else { + documentation = `Source: ${ExtensionName}`; + } + return { label, kind, @@ -80,7 +113,7 @@ export function createCompletionItem( textEdit: textEdit, filterText: filterText, sortText: options?.sortText, - documentation: `${options?.documentation ? `${options?.documentation}\n` : ''}Source: ${ExtensionName}`, + documentation: documentation, data: options?.data, }; } diff --git a/src/autocomplete/IntrinsicFunctionArgumentCompletionProvider.ts b/src/autocomplete/IntrinsicFunctionArgumentCompletionProvider.ts index b74001c6..e0255457 100644 --- a/src/autocomplete/IntrinsicFunctionArgumentCompletionProvider.ts +++ b/src/autocomplete/IntrinsicFunctionArgumentCompletionProvider.ts @@ -12,7 +12,7 @@ import { SchemaRetriever } from '../schema/SchemaRetriever'; import { LoggerFactory } from '../telemetry/LoggerFactory'; import { getFuzzySearchFunction } from '../utils/FuzzySearchUtil'; import { CompletionProvider } from './CompletionProvider'; -import { createCompletionItem, createReplacementRange } from './CompletionUtils'; +import { createCompletionItem, createMarkupContent, createReplacementRange } from './CompletionUtils'; interface IntrinsicFunctionInfo { type: IntrinsicFunction; @@ -344,10 +344,25 @@ export class IntrinsicFunctionArgumentCompletionProvider implements CompletionPr const attributes = this.getResourceAttributes(resource.Type); for (const attributeName of attributes) { + const schema = this.schemaRetriever.getDefault().schemas.get(resource.Type); + let attributeDescription = `${attributeName} attribute of ${resource.Type}`; + + if (schema) { + const jsonPointerPath = `/properties/${attributeName.replaceAll('.', '/')}`; + + try { + const resolvedSchemas = schema.resolveJsonPointerPath(jsonPointerPath); + if (resolvedSchemas.length > 0 && resolvedSchemas[0].description) { + attributeDescription = resolvedSchemas[0].description; + } + } catch (error) { + log.error({ error }, 'Error resolving JSON Pointer path'); + } + } completionItems.push( createCompletionItem(`${resourceName}.${attributeName}`, CompletionItemKind.Property, { detail: `GetAtt (${resource.Type})`, - documentation: `Get attribute ${attributeName} from resource ${resourceName}`, + documentation: createMarkupContent(attributeDescription), data: { isIntrinsicFunction: true, }, @@ -648,7 +663,8 @@ export class IntrinsicFunctionArgumentCompletionProvider implements CompletionPr } const resource = resourceContext.entity as Resource; - if (!resource.Type || typeof resource.Type !== 'string') { + const resourceType = resource.Type; + if (!resourceType || typeof resourceType !== 'string') { return undefined; } @@ -658,8 +674,36 @@ export class IntrinsicFunctionArgumentCompletionProvider implements CompletionPr } const completionItems = attributes.map((attributeName) => { + const schema = this.schemaRetriever.getDefault().schemas.get(resourceType); + let documentation; + + if (schema) { + const jsonPointerPath = `/properties/${attributeName.replaceAll('.', '/properties/')}`; + documentation = createMarkupContent( + `**${attributeName}** attribute of **${resource.Type}**\n\nReturns the value of this attribute when used with the GetAtt intrinsic function.`, + ); + + try { + const resolvedSchemas = schema.resolveJsonPointerPath(jsonPointerPath); + + if (resolvedSchemas.length > 0) { + const firstSchema = resolvedSchemas[0]; + + if (firstSchema.description) { + documentation = createMarkupContent(firstSchema.description); + } + } + } catch (error) { + log.debug(error); + } + } + const item = createCompletionItem(attributeName, CompletionItemKind.Property, { - data: { isIntrinsicFunction: true }, + documentation: documentation, + detail: `GetAtt attribute for ${resource.Type}`, + data: { + isIntrinsicFunction: true, + }, }); if (context.text.length > 0) { diff --git a/src/autocomplete/ResourcePropertyCompletionProvider.ts b/src/autocomplete/ResourcePropertyCompletionProvider.ts index 93004c15..d7049558 100644 --- a/src/autocomplete/ResourcePropertyCompletionProvider.ts +++ b/src/autocomplete/ResourcePropertyCompletionProvider.ts @@ -4,13 +4,14 @@ import { Resource } from '../context/semantic/Entity'; import { CfnValue } from '../context/semantic/SemanticTypes'; import { NodeType } from '../context/syntaxtree/utils/NodeType'; import { CommonNodeTypes } from '../context/syntaxtree/utils/TreeSitterTypes'; +import { propertyTypesToMarkdown } from '../hover/HoverFormatter'; import { PropertyType, ResourceSchema } from '../schema/ResourceSchema'; import { SchemaRetriever } from '../schema/SchemaRetriever'; import { getFuzzySearchFunction } from '../utils/FuzzySearchUtil'; import { templatePathToJsonPointerPath } from '../utils/PathUtils'; import { CompletionItemData, ExtendedCompletionItem } from './CompletionFormatter'; import { CompletionProvider } from './CompletionProvider'; -import { createCompletionItem } from './CompletionUtils'; +import { createCompletionItem, createMarkupContent } from './CompletionUtils'; export class ResourcePropertyCompletionProvider implements CompletionProvider { private readonly fuzzySearch = getFuzzySearchFunction(); @@ -237,7 +238,6 @@ export class ResourcePropertyCompletionProvider implements CompletionProvider { context: Context, ): CompletionItem[] { const result: CompletionItem[] = []; - const availableRequiredProperties = [...requiredProperties].filter( (propName) => allProperties.has(propName) && !existingProperties.has(propName), ); @@ -255,10 +255,22 @@ export class ResourcePropertyCompletionProvider implements CompletionProvider { const itemData = this.getPropertyType(schema, propertyDef); + // Generate rich markdown documentation for the property + let documentation; + if (propertyDef.description || propertyDef.properties || propertyDef.type) { + // Use the rich markdown formatter from hover system + const markdownDoc = propertyTypesToMarkdown(propertyName, [propertyDef]); + documentation = createMarkupContent(markdownDoc); + } else { + // Fallback to simple description for properties without schema details + documentation = `${propertyName} property of ${schema.typeName}`; + } + const completionItem: ExtendedCompletionItem = createCompletionItem( propertyName, CompletionItemKind.Property, { + documentation: documentation, data: itemData, context: context, }, diff --git a/tst/unit/autocomplete/CompletionUtils.test.ts b/tst/unit/autocomplete/CompletionUtils.test.ts new file mode 100644 index 00000000..1599149a --- /dev/null +++ b/tst/unit/autocomplete/CompletionUtils.test.ts @@ -0,0 +1,94 @@ +import { describe, expect, test } from 'vitest'; +import { CompletionItemKind, MarkupContent, MarkupKind } from 'vscode-languageserver'; +import { createCompletionItem } from '../../../src/autocomplete/CompletionUtils'; + +describe('CompletionUtils', () => { + describe('createCompletionItem', () => { + describe('documentation handling', () => { + const stringDoc = 'This is a test documentation'; + const expectedStringResult = 'This is a test documentation\n\nSource: AWS CloudFormation'; + + const markupDoc = { kind: MarkupKind.Markdown, value: '**Bold** documentation' }; + const expectedMarkupResult = { + kind: MarkupKind.Markdown, + value: '**Bold** documentation\n\n**Source:** AWS CloudFormation', + }; + + const emptyDoc = ''; + const defaultSource = 'Source: AWS CloudFormation'; + + const markdownDoc = { kind: MarkupKind.Markdown, value: '**Bold** text' }; + const plainTextDoc = { kind: MarkupKind.PlainText, value: 'Plain text' }; + + const complexMarkdown = { + kind: MarkupKind.Markdown, + value: `# S3 Bucket + + Creates an **Amazon S3** bucket with: + - Versioning enabled + - Public access *blocked* + - [Documentation](https://docs.aws.amazon.com)`, + }; + + const expectedComplexMarkdown = { + kind: MarkupKind.Markdown, + value: `# S3 Bucket + + Creates an **Amazon S3** bucket with: + - Versioning enabled + - Public access *blocked* + - [Documentation](https://docs.aws.amazon.com) + +**Source:** AWS CloudFormation`, + }; + + test('should handle string documentation with source attribution', () => { + const result = createCompletionItem('Test', CompletionItemKind.Keyword, { + documentation: stringDoc, + }); + expect(result.documentation).toEqual(expectedStringResult); + }); + + test('should handle MarkupContent documentation with source attribution', () => { + const result = createCompletionItem('Test', CompletionItemKind.Keyword, { + documentation: markupDoc, + }); + expect(result.documentation).toEqual(expectedMarkupResult); + }); + + test('should handle undefined documentation with default source', () => { + const result = createCompletionItem('Test', CompletionItemKind.Keyword); + expect(result.documentation).toEqual(defaultSource); + }); + + test('should handle empty string documentation', () => { + const result = createCompletionItem('Test', CompletionItemKind.Keyword, { + documentation: emptyDoc, + }); + expect(result.documentation).toEqual(defaultSource); + }); + + test('should preserve MarkupContent kind when adding source attribution', () => { + const markdownResult = createCompletionItem('Test', CompletionItemKind.Keyword, { + documentation: markdownDoc, + }); + const markdownAsMarkup = markdownResult.documentation as MarkupContent; + expect(markdownAsMarkup.kind).toEqual(MarkupKind.Markdown); + + const plainTextResult = createCompletionItem('Test', CompletionItemKind.Keyword, { + documentation: plainTextDoc, + }); + const plainTextAsMarkup = plainTextResult.documentation as MarkupContent; + expect(plainTextAsMarkup.kind).toEqual(MarkupKind.PlainText); + }); + + test('should handle MarkupContent with existing markdown formatting', () => { + const result = createCompletionItem('Test', CompletionItemKind.Keyword, { + documentation: complexMarkdown, + }); + + expect(result.documentation).toEqual(expectedComplexMarkdown); + }); + }); + }); +});