1- import { CompletionItem , CompletionItemKind , CompletionList , InsertTextFormat } from 'vscode-languageserver' ;
1+ import {
2+ CompletionItem ,
3+ CompletionItemKind ,
4+ CompletionList ,
5+ InsertTextFormat ,
6+ Range ,
7+ Position ,
8+ TextEdit ,
9+ } from 'vscode-languageserver' ;
210import { Context } from '../context/Context' ;
311import { ResourceAttributesSet , TopLevelSection , TopLevelSectionsSet } from '../context/ContextType' ;
12+ import { Resource } from '../context/semantic/Entity' ;
13+ import { EntityType } from '../context/semantic/SemanticTypes' ;
414import { NodeType } from '../context/syntaxtree/utils/NodeType' ;
515import { DocumentType } from '../document/Document' ;
16+ import { SchemaRetriever } from '../schema/SchemaRetriever' ;
617import { EditorSettings } from '../settings/Settings' ;
718import { LoggerFactory } from '../telemetry/LoggerFactory' ;
819import { getIndentationString } from '../utils/IndentationUtils' ;
20+ import { RESOURCE_ATTRIBUTE_TYPES } from './CompletionUtils' ;
921
1022export type CompletionItemData = {
1123 type ?: 'object' | 'array' | 'simple' ;
@@ -17,6 +29,8 @@ export interface ExtendedCompletionItem extends CompletionItem {
1729}
1830
1931export class CompletionFormatter {
32+ // In CompletionFormatter class
33+
2034 private static readonly log = LoggerFactory . getLogger ( CompletionFormatter ) ;
2135 private static instance : CompletionFormatter ;
2236
@@ -38,12 +52,18 @@ export class CompletionFormatter {
3852 return `{INDENT${ numberOfIndents } }` ;
3953 }
4054
41- format ( completions : CompletionList , context : Context , editorSettings : EditorSettings ) : CompletionList {
55+ format (
56+ completions : CompletionList ,
57+ context : Context ,
58+ editorSettings : EditorSettings ,
59+ lineContent ?: string ,
60+ schemaRetriever ?: SchemaRetriever ,
61+ ) : CompletionList {
4262 try {
4363 const documentType = context . documentType ;
44-
45- const formattedItems = completions . items . map ( ( item ) => this . formatItem ( item , documentType , editorSettings ) ) ;
46-
64+ const formattedItems = completions . items . map ( ( item ) =>
65+ this . formatItem ( item , documentType , editorSettings , context , lineContent , schemaRetriever ) ,
66+ ) ;
4767 return {
4868 ...completions ,
4969 items : formattedItems ,
@@ -58,6 +78,9 @@ export class CompletionFormatter {
5878 item : CompletionItem ,
5979 documentType : DocumentType ,
6080 editorSettings : EditorSettings ,
81+ context : Context ,
82+ lineContent ?: string ,
83+ schemaRetriever ?: SchemaRetriever ,
6184 ) : CompletionItem {
6285 const formattedItem = { ...item } ;
6386
@@ -66,19 +89,198 @@ export class CompletionFormatter {
6689 return formattedItem ;
6790 }
6891
92+ // Set filterText for ALL items (including snippets) when in JSON with quotes
93+ const isInJsonString = documentType === DocumentType . JSON && context . syntaxNode . type === 'string' ;
94+ if ( isInJsonString ) {
95+ formattedItem . filterText = `"${ context . text } "` ;
96+ }
97+
6998 const textToFormat = item . insertText ?? item . label ;
7099
71100 if ( documentType === DocumentType . JSON ) {
72- formattedItem . insertText = this . formatForJson ( textToFormat ) ;
101+ const result = this . formatForJson (
102+ editorSettings ,
103+ textToFormat ,
104+ item ,
105+ context ,
106+ lineContent ,
107+ schemaRetriever ,
108+ ) ;
109+ formattedItem . textEdit = TextEdit . replace ( result . range , result . text ) ;
110+ if ( result . isSnippet ) {
111+ formattedItem . insertTextFormat = InsertTextFormat . Snippet ;
112+ }
113+ delete formattedItem . insertText ;
73114 } else {
74115 formattedItem . insertText = this . formatForYaml ( textToFormat , item , editorSettings ) ;
75116 }
76117
77118 return formattedItem ;
78119 }
79120
80- private formatForJson ( label : string ) : string {
81- return label ;
121+ private formatForJson (
122+ editorSettings : EditorSettings ,
123+ label : string ,
124+ item : CompletionItem ,
125+ context : Context ,
126+ lineContent ?: string ,
127+ schemaRetriever ?: SchemaRetriever ,
128+ ) : { text : string ; range : Range ; isSnippet : boolean } {
129+ const shouldFormat = context . syntaxNode . type === 'string' && ! context . isValue ( ) && lineContent ;
130+
131+ const itemData = item . data as CompletionItemData | undefined ;
132+
133+ let formatAsObject = itemData ?. type === 'object' ;
134+ let formatAsArray = itemData ?. type === 'array' ;
135+ let formatAsString = false ;
136+
137+ if ( this . isTopLevelSection ( label ) ) {
138+ if ( label === String ( TopLevelSection . Description ) ) {
139+ formatAsString = true ;
140+ } else {
141+ formatAsObject = true ;
142+ }
143+ }
144+ // If type is not in item.data and we have schemaRetriever, look it up from schema
145+ if ( ( ! itemData ?. type || itemData ?. type === 'simple' ) && schemaRetriever && context . entity ) {
146+ const propertyType = this . getPropertyTypeFromSchema ( schemaRetriever , context , label ) ;
147+
148+ switch ( propertyType ) {
149+ case 'object' : {
150+ formatAsObject = true ;
151+ break ;
152+ }
153+ case 'array' : {
154+ formatAsArray = true ;
155+
156+ break ;
157+ }
158+ case 'string' : {
159+ formatAsString = true ;
160+
161+ break ;
162+ }
163+ // No default
164+ }
165+ }
166+
167+ const indentation = ' ' . repeat ( context . startPosition . column ) ;
168+ const indentString = getIndentationString ( editorSettings , DocumentType . JSON ) ;
169+
170+ let replacementText = `${ indentation } "${ label } ":` ;
171+ let isSnippet = false ;
172+
173+ if ( shouldFormat ) {
174+ isSnippet = true ;
175+ if ( formatAsObject ) {
176+ replacementText = `${ indentation } "${ label } ": {\n${ indentation } ${ indentString } $0\n${ indentation } }` ;
177+ } else if ( formatAsArray ) {
178+ replacementText = `${ indentation } "${ label } ": [\n${ indentation } ${ indentString } $0\n${ indentation } ]` ;
179+ } else if ( formatAsString ) {
180+ replacementText = `${ indentation } "${ label } ": "$0"` ;
181+ }
182+ }
183+
184+ const range = Range . create (
185+ Position . create ( context . startPosition . row , 0 ) ,
186+ Position . create ( context . endPosition . row , context . endPosition . column + 1 ) ,
187+ ) ;
188+
189+ return {
190+ text : replacementText ,
191+ range : range ,
192+ isSnippet : isSnippet ,
193+ } ;
194+ }
195+
196+ /**
197+ * Get the type of a property from the CloudFormation schema
198+ * @param schemaRetriever - SchemaRetriever instance to get schemas
199+ * @param context - Current context with entity and property path information
200+ * @param propertyName - Name of the property to look up
201+ * @returns The first type found in the schema ('object', 'array', 'string', etc.) or undefined
202+ */
203+ private getPropertyTypeFromSchema (
204+ schemaRetriever : SchemaRetriever ,
205+ context : Context ,
206+ propertyName : string ,
207+ ) : string | undefined {
208+ let resourceSchema ;
209+
210+ if ( ResourceAttributesSet . has ( propertyName ) ) {
211+ return RESOURCE_ATTRIBUTE_TYPES [ propertyName ] ;
212+ }
213+
214+ const entity = context . entity ;
215+ if ( ! entity || context . getEntityType ( ) !== EntityType . Resource ) {
216+ return undefined ;
217+ }
218+
219+ const resourceType = ( entity as Resource ) . Type ;
220+ if ( ! resourceType ) {
221+ return undefined ;
222+ }
223+
224+ try {
225+ const combinedSchemas = schemaRetriever . getDefault ( ) ;
226+
227+ resourceSchema = combinedSchemas . schemas . get ( resourceType ) ;
228+ if ( ! resourceSchema ) {
229+ return undefined ;
230+ }
231+ } catch {
232+ return undefined ;
233+ }
234+
235+ const propertiesIndex = context . propertyPath . indexOf ( 'Properties' ) ;
236+ let propertyPath : string [ ] ;
237+
238+ if ( propertiesIndex === - 1 ) {
239+ propertyPath = [ propertyName ] ;
240+ } else {
241+ const pathAfterProperties = context . propertyPath . slice ( propertiesIndex + 1 ) . map ( String ) ;
242+
243+ if (
244+ pathAfterProperties . length > 0 &&
245+ pathAfterProperties [ pathAfterProperties . length - 1 ] === context . text
246+ ) {
247+ propertyPath = [ ...pathAfterProperties . slice ( 0 , - 1 ) , propertyName ] ;
248+ } else if ( pathAfterProperties [ pathAfterProperties . length - 1 ] === propertyName ) {
249+ propertyPath = pathAfterProperties ;
250+ } else {
251+ propertyPath = [ ...pathAfterProperties , propertyName ] ;
252+ }
253+ }
254+
255+ // Build JSON pointer path using wildcard notation for array indices
256+ // CloudFormation schemas use /properties/Tags/*/Key format for array item properties
257+ const schemaPath = propertyPath . map ( ( part ) => ( Number . isNaN ( Number ( part ) ) ? part : '*' ) ) ;
258+ const jsonPointerParts = [ 'properties' , ...schemaPath ] ;
259+
260+ const jsonPointerPath = '/' + jsonPointerParts . join ( '/' ) ;
261+
262+ try {
263+ const propertyDefinitions = resourceSchema . resolveJsonPointerPath ( jsonPointerPath ) ;
264+
265+ if ( propertyDefinitions . length === 0 ) {
266+ return undefined ;
267+ }
268+
269+ const propertyDef = propertyDefinitions [ 0 ] ;
270+
271+ if ( propertyDef && 'type' in propertyDef ) {
272+ const type = propertyDef . type ;
273+ if ( Array . isArray ( type ) ) {
274+ return type [ 0 ] ;
275+ } else if ( typeof type === 'string' ) {
276+ return type ;
277+ }
278+ }
279+
280+ return undefined ;
281+ } catch {
282+ return undefined ;
283+ }
82284 }
83285
84286 private formatForYaml ( label : string , item : CompletionItem | undefined , editorSettings : EditorSettings ) : string {
0 commit comments