Skip to content

Commit fc8795e

Browse files
authored
Add description to autocomplete (#68)
* Adding description to autocomplete suggestion * Add description markdowns on autocomplete for property types and getAtt attributes
1 parent 8019527 commit fc8795e

File tree

4 files changed

+191
-8
lines changed

4 files changed

+191
-8
lines changed

src/autocomplete/CompletionUtils.ts

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import {
44
CompletionParams,
55
InsertTextFormat,
66
InsertTextMode,
7+
MarkupContent,
8+
MarkupKind,
79
Position,
810
Range,
911
TextEdit,
@@ -33,6 +35,20 @@ export function createReplacementRange(context: Context, includeQuotes?: boolean
3335
}
3436
}
3537

38+
/**
39+
* Creates a MarkupContent object from a markdown string.
40+
* This ensures consistent formatting for completion documentation.
41+
*
42+
* @param markdown The markdown content string
43+
* @returns A MarkupContent object with markdown formatting
44+
*/
45+
export function createMarkupContent(markdown: string): MarkupContent {
46+
return {
47+
kind: MarkupKind.Markdown,
48+
value: markdown,
49+
};
50+
}
51+
3652
/**
3753
* Creates a base completion item with common properties.
3854
* This reduces duplication across different completion providers.
@@ -51,7 +67,7 @@ export function createCompletionItem(
5167
insertTextFormat?: InsertTextFormat;
5268
insertTextMode?: InsertTextMode;
5369
sortText?: string;
54-
documentation?: string;
70+
documentation?: string | MarkupContent;
5571
data?: Record<string, unknown>;
5672
context?: Context;
5773
},
@@ -70,6 +86,23 @@ export function createCompletionItem(
7086
}
7187
}
7288

89+
// Handle documentation - support both string and MarkupContent
90+
let documentation: string | MarkupContent | undefined;
91+
if (options?.documentation) {
92+
if (typeof options.documentation === 'string') {
93+
// For string documentation, add the source attribution
94+
documentation = `${options.documentation}\n\nSource: ${ExtensionName}`;
95+
} else {
96+
// For MarkupContent, add source attribution to the markdown value
97+
documentation = {
98+
kind: options.documentation.kind,
99+
value: `${options.documentation.value}\n\n**Source:** ${ExtensionName}`,
100+
};
101+
}
102+
} else {
103+
documentation = `Source: ${ExtensionName}`;
104+
}
105+
73106
return {
74107
label,
75108
kind,
@@ -80,7 +113,7 @@ export function createCompletionItem(
80113
textEdit: textEdit,
81114
filterText: filterText,
82115
sortText: options?.sortText,
83-
documentation: `${options?.documentation ? `${options?.documentation}\n` : ''}Source: ${ExtensionName}`,
116+
documentation: documentation,
84117
data: options?.data,
85118
};
86119
}

src/autocomplete/IntrinsicFunctionArgumentCompletionProvider.ts

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { SchemaRetriever } from '../schema/SchemaRetriever';
1212
import { LoggerFactory } from '../telemetry/LoggerFactory';
1313
import { getFuzzySearchFunction } from '../utils/FuzzySearchUtil';
1414
import { CompletionProvider } from './CompletionProvider';
15-
import { createCompletionItem, createReplacementRange } from './CompletionUtils';
15+
import { createCompletionItem, createMarkupContent, createReplacementRange } from './CompletionUtils';
1616

1717
interface IntrinsicFunctionInfo {
1818
type: IntrinsicFunction;
@@ -344,10 +344,25 @@ export class IntrinsicFunctionArgumentCompletionProvider implements CompletionPr
344344

345345
const attributes = this.getResourceAttributes(resource.Type);
346346
for (const attributeName of attributes) {
347+
const schema = this.schemaRetriever.getDefault().schemas.get(resource.Type);
348+
let attributeDescription = `${attributeName} attribute of ${resource.Type}`;
349+
350+
if (schema) {
351+
const jsonPointerPath = `/properties/${attributeName.replaceAll('.', '/')}`;
352+
353+
try {
354+
const resolvedSchemas = schema.resolveJsonPointerPath(jsonPointerPath);
355+
if (resolvedSchemas.length > 0 && resolvedSchemas[0].description) {
356+
attributeDescription = resolvedSchemas[0].description;
357+
}
358+
} catch (error) {
359+
log.error({ error }, 'Error resolving JSON Pointer path');
360+
}
361+
}
347362
completionItems.push(
348363
createCompletionItem(`${resourceName}.${attributeName}`, CompletionItemKind.Property, {
349364
detail: `GetAtt (${resource.Type})`,
350-
documentation: `Get attribute ${attributeName} from resource ${resourceName}`,
365+
documentation: createMarkupContent(attributeDescription),
351366
data: {
352367
isIntrinsicFunction: true,
353368
},
@@ -648,7 +663,8 @@ export class IntrinsicFunctionArgumentCompletionProvider implements CompletionPr
648663
}
649664

650665
const resource = resourceContext.entity as Resource;
651-
if (!resource.Type || typeof resource.Type !== 'string') {
666+
const resourceType = resource.Type;
667+
if (!resourceType || typeof resourceType !== 'string') {
652668
return undefined;
653669
}
654670

@@ -658,8 +674,36 @@ export class IntrinsicFunctionArgumentCompletionProvider implements CompletionPr
658674
}
659675

660676
const completionItems = attributes.map((attributeName) => {
677+
const schema = this.schemaRetriever.getDefault().schemas.get(resourceType);
678+
let documentation;
679+
680+
if (schema) {
681+
const jsonPointerPath = `/properties/${attributeName.replaceAll('.', '/properties/')}`;
682+
documentation = createMarkupContent(
683+
`**${attributeName}** attribute of **${resource.Type}**\n\nReturns the value of this attribute when used with the GetAtt intrinsic function.`,
684+
);
685+
686+
try {
687+
const resolvedSchemas = schema.resolveJsonPointerPath(jsonPointerPath);
688+
689+
if (resolvedSchemas.length > 0) {
690+
const firstSchema = resolvedSchemas[0];
691+
692+
if (firstSchema.description) {
693+
documentation = createMarkupContent(firstSchema.description);
694+
}
695+
}
696+
} catch (error) {
697+
log.debug(error);
698+
}
699+
}
700+
661701
const item = createCompletionItem(attributeName, CompletionItemKind.Property, {
662-
data: { isIntrinsicFunction: true },
702+
documentation: documentation,
703+
detail: `GetAtt attribute for ${resource.Type}`,
704+
data: {
705+
isIntrinsicFunction: true,
706+
},
663707
});
664708

665709
if (context.text.length > 0) {

src/autocomplete/ResourcePropertyCompletionProvider.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@ import { Resource } from '../context/semantic/Entity';
44
import { CfnValue } from '../context/semantic/SemanticTypes';
55
import { NodeType } from '../context/syntaxtree/utils/NodeType';
66
import { CommonNodeTypes } from '../context/syntaxtree/utils/TreeSitterTypes';
7+
import { propertyTypesToMarkdown } from '../hover/HoverFormatter';
78
import { PropertyType, ResourceSchema } from '../schema/ResourceSchema';
89
import { SchemaRetriever } from '../schema/SchemaRetriever';
910
import { getFuzzySearchFunction } from '../utils/FuzzySearchUtil';
1011
import { templatePathToJsonPointerPath } from '../utils/PathUtils';
1112
import { CompletionItemData, ExtendedCompletionItem } from './CompletionFormatter';
1213
import { CompletionProvider } from './CompletionProvider';
13-
import { createCompletionItem } from './CompletionUtils';
14+
import { createCompletionItem, createMarkupContent } from './CompletionUtils';
1415

1516
export class ResourcePropertyCompletionProvider implements CompletionProvider {
1617
private readonly fuzzySearch = getFuzzySearchFunction();
@@ -237,7 +238,6 @@ export class ResourcePropertyCompletionProvider implements CompletionProvider {
237238
context: Context,
238239
): CompletionItem[] {
239240
const result: CompletionItem[] = [];
240-
241241
const availableRequiredProperties = [...requiredProperties].filter(
242242
(propName) => allProperties.has(propName) && !existingProperties.has(propName),
243243
);
@@ -255,10 +255,22 @@ export class ResourcePropertyCompletionProvider implements CompletionProvider {
255255

256256
const itemData = this.getPropertyType(schema, propertyDef);
257257

258+
// Generate rich markdown documentation for the property
259+
let documentation;
260+
if (propertyDef.description || propertyDef.properties || propertyDef.type) {
261+
// Use the rich markdown formatter from hover system
262+
const markdownDoc = propertyTypesToMarkdown(propertyName, [propertyDef]);
263+
documentation = createMarkupContent(markdownDoc);
264+
} else {
265+
// Fallback to simple description for properties without schema details
266+
documentation = `${propertyName} property of ${schema.typeName}`;
267+
}
268+
258269
const completionItem: ExtendedCompletionItem = createCompletionItem(
259270
propertyName,
260271
CompletionItemKind.Property,
261272
{
273+
documentation: documentation,
262274
data: itemData,
263275
context: context,
264276
},
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { describe, expect, test } from 'vitest';
2+
import { CompletionItemKind, MarkupContent, MarkupKind } from 'vscode-languageserver';
3+
import { createCompletionItem } from '../../../src/autocomplete/CompletionUtils';
4+
5+
describe('CompletionUtils', () => {
6+
describe('createCompletionItem', () => {
7+
describe('documentation handling', () => {
8+
const stringDoc = 'This is a test documentation';
9+
const expectedStringResult = 'This is a test documentation\n\nSource: AWS CloudFormation';
10+
11+
const markupDoc = { kind: MarkupKind.Markdown, value: '**Bold** documentation' };
12+
const expectedMarkupResult = {
13+
kind: MarkupKind.Markdown,
14+
value: '**Bold** documentation\n\n**Source:** AWS CloudFormation',
15+
};
16+
17+
const emptyDoc = '';
18+
const defaultSource = 'Source: AWS CloudFormation';
19+
20+
const markdownDoc = { kind: MarkupKind.Markdown, value: '**Bold** text' };
21+
const plainTextDoc = { kind: MarkupKind.PlainText, value: 'Plain text' };
22+
23+
const complexMarkdown = {
24+
kind: MarkupKind.Markdown,
25+
value: `# S3 Bucket
26+
27+
Creates an **Amazon S3** bucket with:
28+
- Versioning enabled
29+
- Public access *blocked*
30+
- [Documentation](https://docs.aws.amazon.com)`,
31+
};
32+
33+
const expectedComplexMarkdown = {
34+
kind: MarkupKind.Markdown,
35+
value: `# S3 Bucket
36+
37+
Creates an **Amazon S3** bucket with:
38+
- Versioning enabled
39+
- Public access *blocked*
40+
- [Documentation](https://docs.aws.amazon.com)
41+
42+
**Source:** AWS CloudFormation`,
43+
};
44+
45+
test('should handle string documentation with source attribution', () => {
46+
const result = createCompletionItem('Test', CompletionItemKind.Keyword, {
47+
documentation: stringDoc,
48+
});
49+
expect(result.documentation).toEqual(expectedStringResult);
50+
});
51+
52+
test('should handle MarkupContent documentation with source attribution', () => {
53+
const result = createCompletionItem('Test', CompletionItemKind.Keyword, {
54+
documentation: markupDoc,
55+
});
56+
expect(result.documentation).toEqual(expectedMarkupResult);
57+
});
58+
59+
test('should handle undefined documentation with default source', () => {
60+
const result = createCompletionItem('Test', CompletionItemKind.Keyword);
61+
expect(result.documentation).toEqual(defaultSource);
62+
});
63+
64+
test('should handle empty string documentation', () => {
65+
const result = createCompletionItem('Test', CompletionItemKind.Keyword, {
66+
documentation: emptyDoc,
67+
});
68+
expect(result.documentation).toEqual(defaultSource);
69+
});
70+
71+
test('should preserve MarkupContent kind when adding source attribution', () => {
72+
const markdownResult = createCompletionItem('Test', CompletionItemKind.Keyword, {
73+
documentation: markdownDoc,
74+
});
75+
const markdownAsMarkup = markdownResult.documentation as MarkupContent;
76+
expect(markdownAsMarkup.kind).toEqual(MarkupKind.Markdown);
77+
78+
const plainTextResult = createCompletionItem('Test', CompletionItemKind.Keyword, {
79+
documentation: plainTextDoc,
80+
});
81+
const plainTextAsMarkup = plainTextResult.documentation as MarkupContent;
82+
expect(plainTextAsMarkup.kind).toEqual(MarkupKind.PlainText);
83+
});
84+
85+
test('should handle MarkupContent with existing markdown formatting', () => {
86+
const result = createCompletionItem('Test', CompletionItemKind.Keyword, {
87+
documentation: complexMarkdown,
88+
});
89+
90+
expect(result.documentation).toEqual(expectedComplexMarkdown);
91+
});
92+
});
93+
});
94+
});

0 commit comments

Comments
 (0)