Skip to content

Commit 20fb05c

Browse files
authored
Merge branch 'main' into nested-objects-fix
2 parents c7289c2 + 86fa9be commit 20fb05c

25 files changed

+512
-324
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ tools/*
1212
!tools/*.ts
1313
**/.aws-cfn-storage
1414
/oss-attribution
15+
/tmp-tst

src/autocomplete/CompletionRouter.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { CfnLspProviders } from '../server/CfnLspProviders';
1313
import { SettingsConfigurable, ISettingsSubscriber, SettingsSubscription } from '../settings/ISettingsSubscriber';
1414
import { CompletionSettings, DefaultSettings } from '../settings/Settings';
1515
import { LoggerFactory } from '../telemetry/LoggerFactory';
16+
import { Track } from '../telemetry/TelemetryDecorator';
1617
import { Closeable } from '../utils/Closeable';
1718
import { CompletionFormatter } from './CompletionFormatter';
1819
import { CompletionProvider } from './CompletionProvider';
@@ -45,6 +46,7 @@ export class CompletionRouter implements SettingsConfigurable, Closeable {
4546
private readonly entityFieldCompletionProviderMap = createEntityFieldProviders(),
4647
) {}
4748

49+
@Track({ name: 'getCompletions' })
4850
getCompletions(params: CompletionParams): Promise<CompletionList> | CompletionList | undefined {
4951
if (!this.completionSettings.enabled) return;
5052

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/InlineCompletionRouter.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { RelationshipSchemaService } from '../services/RelationshipSchemaService
77
import { SettingsConfigurable, ISettingsSubscriber, SettingsSubscription } from '../settings/ISettingsSubscriber';
88
import { CompletionSettings, DefaultSettings } from '../settings/Settings';
99
import { LoggerFactory } from '../telemetry/LoggerFactory';
10+
import { Track } from '../telemetry/TelemetryDecorator';
1011
import { Closeable } from '../utils/Closeable';
1112
import { InlineCompletionProvider } from './InlineCompletionProvider';
1213
import { RelatedResourcesInlineCompletionProvider } from './RelatedResourcesInlineCompletionProvider';
@@ -25,6 +26,7 @@ export class InlineCompletionRouter implements SettingsConfigurable, Closeable {
2526
private readonly documentManager: DocumentManager,
2627
) {}
2728

29+
@Track({ name: 'getInlineCompletions' })
2830
getInlineCompletions(params: InlineCompletionParams): Promise<ReturnType> | ReturnType {
2931
if (!this.completionSettings.enabled) return;
3032

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
},

src/context/ContextManager.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { SyntaxNode } from 'tree-sitter';
22
import { TextDocumentPositionParams } from 'vscode-languageserver-protocol/lib/common/protocol';
33
import { LoggerFactory } from '../telemetry/LoggerFactory';
4+
import { Track } from '../telemetry/TelemetryDecorator';
45
import { extractErrorMessage } from '../utils/Errors';
56
import { Context } from './Context';
67
import { ContextWithRelatedEntities } from './ContextWithRelatedEntities';
@@ -12,6 +13,7 @@ export class ContextManager {
1213

1314
constructor(private readonly syntaxTreeManager: SyntaxTreeManager) {}
1415

16+
@Track({ name: 'getContext' })
1517
public getContext(params: TextDocumentPositionParams): Context | undefined {
1618
const contextParams = this.getContextParams(params);
1719
if (!contextParams) {
@@ -40,6 +42,7 @@ export class ContextManager {
4042
return undefined;
4143
}
4244

45+
@Track({ name: 'getContextWithEntities' })
4346
public getContextAndRelatedEntities(
4447
params: TextDocumentPositionParams,
4548
fullEntitySearch: boolean = true,

src/context/FileContextManager.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { DocumentManager } from '../document/DocumentManager';
22
import { LoggerFactory } from '../telemetry/LoggerFactory';
3+
import { Track } from '../telemetry/TelemetryDecorator';
34
import { FileContext } from './FileContext';
45

56
/**
@@ -10,6 +11,7 @@ export class FileContextManager {
1011

1112
constructor(private readonly documentManager: DocumentManager) {}
1213

14+
@Track({ name: 'getFileContext' })
1315
public getFileContext(uri: string): FileContext | undefined {
1416
const document = this.documentManager.get(uri);
1517
if (!document) {

src/context/syntaxtree/SyntaxTreeManager.ts

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Edit, Point } from 'tree-sitter';
22
import { CloudFormationFileType, DocumentType } from '../../document/Document';
33
import { detectDocumentType } from '../../document/DocumentUtils';
44
import { LoggerFactory } from '../../telemetry/LoggerFactory';
5+
import { Measure } from '../../telemetry/TelemetryDecorator';
56
import { extractErrorMessage } from '../../utils/Errors';
67
import { JsonSyntaxTree } from './JsonSyntaxTree';
78
import { SyntaxTree } from './SyntaxTree';
@@ -27,26 +28,28 @@ export class SyntaxTreeManager {
2728
return;
2829
}
2930

30-
this.log.info({ type, cfnFileType }, `Created tree ${uri}`);
3131
try {
32-
if (type === DocumentType.YAML) {
33-
this.createYamlSyntaxTree(uri, content);
34-
} else {
35-
this.createJsonSyntaxTree(uri, content);
36-
}
37-
this.log.debug(
38-
{
39-
uri,
40-
type,
41-
cfnFileType,
42-
},
43-
'Created SyntaxTree',
44-
);
32+
this.createTree(uri, content, type, cfnFileType);
4533
} catch (error) {
4634
logger.error(`Failed to create tree ${uri} ${type} ${cfnFileType}: ${extractErrorMessage(error)}`);
4735
}
4836
}
4937

38+
@Measure({ name: 'createTree' })
39+
private createTree(uri: string, content: string, type: DocumentType, cfnFileType: CloudFormationFileType) {
40+
if (cfnFileType !== CloudFormationFileType.Template) {
41+
throw new Error('Syntax tree can only be created for CloudFormation templates');
42+
}
43+
44+
if (type === DocumentType.YAML) {
45+
this.createYamlSyntaxTree(uri, content);
46+
} else {
47+
this.createJsonSyntaxTree(uri, content);
48+
}
49+
50+
this.log.info({ type, cfnFileType }, `Created tree ${uri}`);
51+
}
52+
5053
private createJsonSyntaxTree(uri: string, content: string) {
5154
this.syntaxTrees.set(uri, new JsonSyntaxTree(content));
5255
}

src/datastore/LMDB.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { open, Database, RootDatabase } from 'lmdb';
2+
import { ScopedTelemetry } from '../telemetry/ScopedTelemetry';
3+
import { Telemetry } from '../telemetry/TelemetryDecorator';
24
import { pathToArtifact } from '../utils/ArtifactsDir';
35
import { DataStore, DataStoreFactory } from './DataStore';
46

@@ -31,6 +33,8 @@ export class LMDBStore implements DataStore {
3133
}
3234

3335
export class LMDBStoreFactory implements DataStoreFactory {
36+
@Telemetry() private readonly telemetry!: ScopedTelemetry;
37+
3438
private readonly rootDir = pathToArtifact('lmdb');
3539
private readonly storePath = `${this.rootDir}/${Version}`;
3640

@@ -43,6 +47,10 @@ export class LMDBStoreFactory implements DataStoreFactory {
4347

4448
private readonly stores = new Map<string, LMDBStore>();
4549

50+
constructor() {
51+
this.registerLMDBGauges();
52+
}
53+
4654
getOrCreate(store: string): DataStore {
4755
let val = this.stores.get(store);
4856
if (val === undefined) {
@@ -85,6 +93,16 @@ export class LMDBStoreFactory implements DataStoreFactory {
8593
this.stores.clear();
8694
await this.env.close();
8795
}
96+
97+
private registerLMDBGauges(): void {
98+
this.telemetry.registerGaugeProvider('lmdb.global.size_mb', () => stats(this.env).totalSizeMB, { unit: 'MB' });
99+
this.telemetry.registerGaugeProvider('lmdb.global.max_size_mb', () => stats(this.env).maxSizeMB, {
100+
unit: 'MB',
101+
});
102+
this.telemetry.registerGaugeProvider('lmdb.global.entries', () => stats(this.env).entries, { unit: '1' });
103+
this.telemetry.registerGaugeProvider('lmdb.global.readers', () => stats(this.env).numReaders, { unit: '1' });
104+
this.telemetry.registerGaugeProvider('lmdb.stores.count', () => this.stores.size, { unit: '1' });
105+
}
88106
}
89107

90108
function bytesToMB(bytes: number) {

0 commit comments

Comments
 (0)