1
- import * as fs from 'node:fs' ;
1
+ import * as fs from 'node:fs/promises ' ;
2
2
import * as path from 'node:path' ;
3
3
import * as vscode from 'vscode' ;
4
4
import { CompletionItem , CompletionItemKind , type Disposable , Hover , MarkdownString } from 'vscode' ;
@@ -15,22 +15,34 @@ interface IamActionData {
15
15
}
16
16
17
17
class IamActionMappings {
18
- private iamActionsMap : Map < string , IamActionData > = new Map ( ) ;
19
- private servicePrefixMap : Map < string , string [ ] > = new Map ( ) ;
18
+ private iamActionsMap : Map < string , IamActionData > | null = null ;
19
+ private servicePrefixMap : Map < string , Set < string > > | null = null ;
20
+ private loadingPromise : Promise < void > | null = null ;
20
21
21
- constructor ( ) {
22
- this . loadIamActionsMap ( ) ;
22
+ private async ensureDataLoaded ( ) : Promise < void > {
23
+ if ( ! this . loadingPromise ) {
24
+ this . loadingPromise = this . loadIamActionsMap ( ) ;
25
+ }
26
+ await this . loadingPromise ;
23
27
}
24
28
25
- private loadIamActionsMap ( ) {
29
+ private async loadIamActionsMap ( ) : Promise < void > {
26
30
const filePath = path . join ( __dirname , '..' , 'snippets' , 'iam-actions.json' ) ;
27
31
try {
28
- const rawData = fs . readFileSync ( filePath , 'utf8' ) ;
32
+ const rawData = await fs . readFile ( filePath , 'utf8' ) ;
29
33
const jsonData = JSON . parse ( rawData ) ;
30
34
35
+ this . iamActionsMap = new Map ( ) ;
36
+ this . servicePrefixMap = new Map ( ) ;
37
+
31
38
for ( const service in jsonData ) {
32
39
const servicePrefix = jsonData [ service ] . service_prefix ;
33
- this . servicePrefixMap . set ( servicePrefix , [ ] ) ;
40
+ let servicePrefixSet = this . servicePrefixMap . get ( servicePrefix ) ;
41
+
42
+ if ( ! servicePrefixSet ) {
43
+ servicePrefixSet = new Set < string > ( ) ;
44
+ this . servicePrefixMap . set ( servicePrefix , servicePrefixSet ) ;
45
+ }
34
46
35
47
for ( const action in jsonData [ service ] . actions ) {
36
48
const actionData = jsonData [ service ] . actions [ action ] ;
@@ -39,12 +51,7 @@ class IamActionMappings {
39
51
condition_keys : actionData . condition_keys || [ ] ,
40
52
resource_types : actionData . resource_types || [ ] ,
41
53
} ) ;
42
- const servicePrefixActions = this . servicePrefixMap . get ( servicePrefix ) ;
43
- if ( servicePrefixActions ) {
44
- servicePrefixActions . push ( actionData . action_name ) ;
45
- } else {
46
- this . servicePrefixMap . set ( servicePrefix , [ actionData . action_name ] ) ;
47
- }
54
+ servicePrefixSet . add ( actionData . action_name ) ;
48
55
}
49
56
}
50
57
@@ -56,19 +63,38 @@ class IamActionMappings {
56
63
}
57
64
}
58
65
59
- public getIamActionData ( action : string ) : IamActionData | undefined {
60
- return this . iamActionsMap . get ( action ) ;
66
+ public async getIamActionData ( action : string ) : Promise < IamActionData | undefined > {
67
+ await this . ensureDataLoaded ( ) ;
68
+ if ( this . iamActionsMap ) {
69
+ return this . iamActionsMap . get ( action ) ;
70
+ }
71
+ return undefined ;
61
72
}
62
73
63
- public getMatchingActions ( wildcardAction : string ) : string [ ] {
74
+ public async getMatchingActions ( wildcardAction : string ) : Promise < string [ ] > {
75
+ await this . ensureDataLoaded ( ) ;
76
+
77
+ if ( ! this . servicePrefixMap ) {
78
+ return [ ] ;
79
+ }
80
+
64
81
const [ servicePrefix , actionPattern ] = wildcardAction . split ( ':' ) ;
65
82
const regex = new RegExp ( `^${ actionPattern . replace ( '*' , '.*' ) } $` ) ;
66
83
67
- const serviceActions = this . servicePrefixMap . get ( servicePrefix ) || [ ] ;
68
- return serviceActions . filter ( ( action ) => regex . test ( action . split ( ':' ) [ 1 ] ) ) ;
84
+ const serviceActions = this . servicePrefixMap . get ( servicePrefix ) || new Set < string > ( ) ;
85
+ return Array . from ( serviceActions ) . filter ( ( action ) => {
86
+ const parts = action . split ( ':' ) ;
87
+ return parts . length > 1 && regex . test ( parts [ 1 ] ) ;
88
+ } ) ;
69
89
}
70
90
71
- public getAllIamActions ( ) : string [ ] {
91
+ public async getAllIamActions ( ) : Promise < string [ ] > {
92
+ await this . ensureDataLoaded ( ) ;
93
+
94
+ if ( ! this . iamActionsMap ) {
95
+ return [ ] ;
96
+ }
97
+
72
98
return Array . from ( this . iamActionsMap . keys ( ) ) ;
73
99
}
74
100
}
@@ -83,13 +109,13 @@ export function activate(context: vscode.ExtensionContext) {
83
109
// Register completion provider
84
110
disposable . push (
85
111
vscode . languages . registerCompletionItemProvider ( [ 'yaml' , 'yml' , 'json' ] , {
86
- provideCompletionItems ( document : vscode . TextDocument , position : vscode . Position ) {
87
- if ( isBelowActionKey ( document , position ) ) {
112
+ async provideCompletionItems ( document : vscode . TextDocument , position : vscode . Position ) {
113
+ if ( await isBelowActionKey ( document , position ) ) {
88
114
outputChannel . appendLine ( `Providing completion items at position: ${ position . line } :${ position . character } ` ) ;
89
- return iamActionMappings
90
- . getAllIamActions ( )
91
- . map ( ( action ) => {
92
- const actionData = iamActionMappings . getIamActionData ( action ) ;
115
+ const allActions = await iamActionMappings . getAllIamActions ( ) ;
116
+ return Promise . all (
117
+ allActions . map ( async ( action ) => {
118
+ const actionData = await iamActionMappings . getIamActionData ( action ) ;
93
119
if ( actionData ) {
94
120
const item = new CompletionItem ( action , CompletionItemKind . Value ) ;
95
121
item . detail = `IAM Action: ${ action . split ( ':' ) [ 1 ] } (${ actionData . access_level } )` ;
@@ -117,8 +143,8 @@ export function activate(context: vscode.ExtensionContext) {
117
143
return item ;
118
144
}
119
145
return null ;
120
- } )
121
- . filter ( ( item ) : item is CompletionItem => item !== null ) ;
146
+ } ) ,
147
+ ) . then ( ( items ) => items . filter ( ( item ) : item is CompletionItem => item !== null ) ) ;
122
148
}
123
149
return undefined ;
124
150
} ,
@@ -128,7 +154,7 @@ export function activate(context: vscode.ExtensionContext) {
128
154
// Register hover provider
129
155
disposable . push (
130
156
vscode . languages . registerHoverProvider ( [ 'yaml' , 'yml' , 'json' ] , {
131
- provideHover ( document : vscode . TextDocument , position : vscode . Position ) {
157
+ async provideHover ( document : vscode . TextDocument , position : vscode . Position ) {
132
158
const actionRegex = / [ a - z A - Z 0 - 9 ] + : [ a - z A - Z 0 - 9 * ] + / ;
133
159
const range = document . getWordRangeAtPosition ( position , actionRegex ) ;
134
160
@@ -137,7 +163,7 @@ export function activate(context: vscode.ExtensionContext) {
137
163
outputChannel . appendLine ( `Providing hover for: ${ word } ` ) ;
138
164
139
165
if ( word . includes ( '*' ) ) {
140
- const matchingActions = iamActionMappings . getMatchingActions ( word ) ;
166
+ const matchingActions = await iamActionMappings . getMatchingActions ( word ) ;
141
167
if ( matchingActions . length > 0 ) {
142
168
const content = new MarkdownString ( ) ;
143
169
content . isTrusted = true ;
@@ -146,7 +172,7 @@ export function activate(context: vscode.ExtensionContext) {
146
172
content . appendMarkdown ( '|:-------|:------------|\n' ) ;
147
173
148
174
for ( const action of matchingActions ) {
149
- const actionData = iamActionMappings . getIamActionData ( action ) ;
175
+ const actionData = await iamActionMappings . getIamActionData ( action ) ;
150
176
if ( actionData ) {
151
177
const description = actionData . description . replace ( / \n / g, ' ' ) ;
152
178
const actionColumn = `[${ action } ](${ actionData . url } ) (${ actionData . access_level } )` ;
@@ -156,7 +182,7 @@ export function activate(context: vscode.ExtensionContext) {
156
182
return new Hover ( content ) ;
157
183
}
158
184
} else {
159
- const actionData = iamActionMappings . getIamActionData ( word ) ;
185
+ const actionData = await iamActionMappings . getIamActionData ( word ) ;
160
186
if ( actionData ) {
161
187
const content = new MarkdownString ( ) ;
162
188
content . isTrusted = true ;
@@ -205,32 +231,35 @@ export function deactivate() {
205
231
outputChannel . appendLine ( 'IAM Action Snippets extension deactivated' ) ;
206
232
}
207
233
208
- function isBelowActionKey ( document : vscode . TextDocument , position : vscode . Position ) : boolean {
234
+ async function isBelowActionKey ( document : vscode . TextDocument , position : vscode . Position ) : Promise < boolean > {
209
235
const maxLinesUp = 10 ;
210
236
const startLine = Math . max ( 0 , position . line - maxLinesUp ) ;
237
+ const text = document . getText ( new vscode . Range ( startLine , 0 , position . line , position . character ) ) ;
211
238
212
- for ( let i = position . line ; i >= startLine ; i -- ) {
213
- const line = document . lineAt ( i ) . text . trim ( ) . toLowerCase ( ) ;
214
-
215
- if ( document . languageId === 'json' ) {
216
- if ( line . includes ( '"action":' ) && line . includes ( '[' ) ) {
217
- return true ;
218
- }
219
- if ( line . includes ( ']' ) ) break ;
220
- } else if ( document . languageId === 'yaml' || document . languageId === 'yml' ) {
239
+ if ( document . languageId === 'json' ) {
240
+ return / " a c t i o n " : \s * \[ / . test ( text ) && ! / \] / . test ( text . split ( '"action":' ) [ 1 ] ) ;
241
+ }
242
+ if ( document . languageId === 'yaml' || document . languageId === 'yml' ) {
243
+ const lines = text . split ( '\n' ) . reverse ( ) ;
244
+ for ( const line of lines ) {
245
+ const trimmedLine = line . trim ( ) . toLowerCase ( ) ;
221
246
if (
222
- line . startsWith ( 'action:' ) ||
223
- line . startsWith ( '- action:' ) ||
224
- line . startsWith ( 'notaction:' ) ||
225
- line . startsWith ( '- notaction:' )
247
+ trimmedLine . startsWith ( 'action:' ) ||
248
+ trimmedLine . startsWith ( '- action:' ) ||
249
+ trimmedLine . startsWith ( 'notaction:' ) ||
250
+ trimmedLine . startsWith ( '- notaction:' )
226
251
) {
227
252
return true ;
228
253
}
229
- if ( line !== '' && ! line . startsWith ( '-' ) && ! line . startsWith ( '- ' ) && ! line . startsWith ( '#' ) ) {
254
+ if (
255
+ trimmedLine !== '' &&
256
+ ! trimmedLine . startsWith ( '-' ) &&
257
+ ! trimmedLine . startsWith ( '- ' ) &&
258
+ ! trimmedLine . startsWith ( '#' )
259
+ ) {
230
260
break ;
231
261
}
232
262
}
233
263
}
234
-
235
264
return false ;
236
265
}
0 commit comments