11import * as fs from 'node:fs/promises' ;
22import * as path from 'node:path' ;
33import * as vscode from 'vscode' ;
4- import { CompletionItem , CompletionItemKind , type Disposable , Hover , MarkdownString } from 'vscode' ;
4+ import { type Disposable , Hover , MarkdownString } from 'vscode' ;
55
6- let outputChannel : vscode . OutputChannel ;
6+ const DEBUG_ENABLED = false ;
7+ export const outputChannel = vscode . window . createOutputChannel ( 'IAM Action Snippets' ) ;
8+
9+ export function log ( message : string ) {
10+ if ( DEBUG_ENABLED ) {
11+ outputChannel . appendLine ( `[DEBUG] ${ message } ` ) ;
12+ }
13+ }
714
815interface IamActionData {
916 access_level : string ;
@@ -55,11 +62,9 @@ class IamActionMappings {
5562 }
5663 }
5764
58- outputChannel . appendLine (
59- `Loaded ${ this . iamActionsMap . size } IAM actions across ${ this . servicePrefixMap . size } services` ,
60- ) ;
65+ log ( `Loaded ${ this . iamActionsMap . size } IAM actions across ${ this . servicePrefixMap . size } services` ) ;
6166 } catch ( error ) {
62- outputChannel . appendLine ( `Error loading IAM actions: ${ error } ` ) ;
67+ log ( `Error loading IAM actions: ${ error } ` ) ;
6368 }
6469 }
6570
@@ -100,8 +105,11 @@ class IamActionMappings {
100105}
101106
102107export function activate ( context : vscode . ExtensionContext ) {
103- outputChannel = vscode . window . createOutputChannel ( 'IAM Action Snippets' ) ;
104- outputChannel . appendLine ( 'IAM Action Snippets extension activated' ) ;
108+ if ( DEBUG_ENABLED ) {
109+ log ( 'IAM Action Snippets extension activated (debug mode)' ) ;
110+ } else {
111+ outputChannel . appendLine ( 'IAM Action Snippets extension activated' ) ;
112+ }
105113
106114 const iamActionMappings = new IamActionMappings ( ) ;
107115 const disposable : Disposable [ ] = [ ] ;
@@ -113,7 +121,7 @@ export function activate(context: vscode.ExtensionContext) {
113121 {
114122 async provideCompletionItems ( document : vscode . TextDocument , position : vscode . Position ) {
115123 if ( await isBelowActionKey ( document , position ) ) {
116- outputChannel . appendLine ( `Providing completion items at position: ${ position . line } :${ position . character } ` ) ;
124+ log ( `Providing completion items at position: ${ position . line } :${ position . character } ` ) ;
117125 const allActions = await iamActionMappings . getAllIamActions ( ) ;
118126 return Promise . all (
119127 allActions . map ( async ( action ) => {
@@ -251,7 +259,7 @@ export function activate(context: vscode.ExtensionContext) {
251259
252260 if ( range ) {
253261 const word = document . getText ( range ) . replace ( / ^ [ " ' ] | [ " ' ] $ / g, '' ) ;
254- outputChannel . appendLine ( `Providing hover for: ${ word } ` ) ;
262+ log ( `Providing hover for: ${ word } ` ) ;
255263
256264 if ( word . includes ( '*' ) ) {
257265 const matchingActions = await iamActionMappings . getMatchingActions ( word ) ;
@@ -323,77 +331,120 @@ export function activate(context: vscode.ExtensionContext) {
323331}
324332
325333export function deactivate ( ) {
326- outputChannel . appendLine ( 'IAM Action Snippets extension deactivated' ) ;
334+ log ( 'IAM Action Snippets extension deactivated' ) ;
327335}
328336
329337async function isBelowActionKey ( document : vscode . TextDocument , position : vscode . Position ) : Promise < boolean > {
330- const maxLinesUp = 40 ;
331- const startLine = Math . max ( 0 , position . line - maxLinesUp ) ;
332- const text = document . getText ( new vscode . Range ( startLine , 0 , position . line , position . character ) ) ;
338+ // Use the full text from the top of the document until the current position
339+ const text = document . getText ( new vscode . Range ( new vscode . Position ( 0 , 0 ) , position ) ) ;
333340
341+ // JSON and Terraform
334342 if ( document . languageId === 'json' || document . languageId === 'terraform' ) {
335343 const lines = text . split ( '\n' ) . reverse ( ) ;
344+ const jsonRegex = / " a c t i o n " \s * : \s * \[ / i;
345+ const terraformRegex = / a c t i o n \s * = \s * \[ / i;
336346 for ( const line of lines ) {
337- const trimmedLine = line . trim ( ) . toLowerCase ( ) ;
338- if ( document . languageId === 'json' ) {
339- if ( trimmedLine . includes ( '"action"' ) && trimmedLine . includes ( '[' ) ) {
340- return true ;
341- }
342- } else if ( document . languageId === 'terraform' ) {
343- if ( trimmedLine . includes ( 'action' ) && trimmedLine . includes ( '=' ) && trimmedLine . includes ( '[' ) ) {
344- return true ;
345- }
347+ const trimmedLine = line . trim ( ) ;
348+ if ( document . languageId === 'json' && jsonRegex . test ( trimmedLine ) ) {
349+ return true ;
350+ }
351+ if ( document . languageId === 'terraform' && terraformRegex . test ( trimmedLine ) ) {
352+ return true ;
346353 }
347354 if ( trimmedLine . startsWith ( '}' ) || trimmedLine . startsWith ( ']' ) ) {
348355 break ;
349356 }
350357 }
351358 }
352359
360+ // YAML / YML
353361 if ( document . languageId === 'yaml' || document . languageId === 'yml' ) {
354- const lines = text . split ( '\n' ) . reverse ( ) ;
355- for ( const line of lines ) {
356- const trimmedLine = line . trim ( ) . toLowerCase ( ) ;
357- if (
358- trimmedLine . startsWith ( 'action:' ) ||
359- trimmedLine . startsWith ( '- action:' ) ||
360- trimmedLine . startsWith ( 'notaction:' ) ||
361- trimmedLine . startsWith ( '- notaction:' )
362- ) {
363- return true ;
362+ const allLines = document . getText ( ) . split ( '\n' ) ;
363+ const currentLineIndex = position . line ;
364+ const currentLine = allLines [ currentLineIndex ] || '' ;
365+ const currentIndent = currentLine . search ( / \S / ) ;
366+ log ( `[YAML] Current line (${ currentLineIndex } ): ${ currentLine } ` ) ;
367+ log ( `[YAML] Current indent: ${ currentIndent } ` ) ;
368+
369+ // If the current line itself is a key, disable autocomplete.
370+ if ( / ^ [ - \s ] * \w + \s * : / . test ( currentLine ) ) {
371+ log ( '[YAML] Current line appears to be a key, disabling autocomplete' ) ;
372+ return false ;
373+ }
374+
375+ // Scan upward for the nearest parent key with lower indent
376+ for ( let i = currentLineIndex - 1 ; i >= 0 ; i -- ) {
377+ const line = allLines [ i ] ;
378+ const trimmedLine = line . trim ( ) ;
379+ if ( trimmedLine === '' || trimmedLine . startsWith ( '#' ) ) {
380+ continue ;
364381 }
365- if (
366- trimmedLine !== '' &&
367- ! trimmedLine . startsWith ( '-' ) &&
368- ! trimmedLine . startsWith ( '- ' ) &&
369- ! trimmedLine . startsWith ( '#' )
370- ) {
371- break ;
382+ if ( ! line . includes ( ':' ) ) {
383+ continue ;
384+ }
385+ log ( `[YAML] Scanning line ${ i } : ${ line } ` ) ;
386+ const match = line . match ( / ^ ( [ \t ] * ) (?: - \s * ) ? ( \w + ) \s * : / ) ;
387+ if ( match ) {
388+ const parentIndent = match [ 1 ] . length ;
389+ if ( parentIndent < currentIndent ) {
390+ const parentKey = match [ 2 ] . toLowerCase ( ) ;
391+ log ( `[YAML] Found parent key: ${ parentKey } at indent: ${ parentIndent } ` ) ;
392+ if ( parentKey === 'action' ) {
393+ log ( `[YAML] Position is under 'action' key, enabling autocomplete` ) ;
394+ return true ;
395+ }
396+ log ( `[YAML] Parent key is not 'action', stopping search.` ) ;
397+ break ;
398+ }
372399 }
373400 }
374401 }
375402
403+ // CDK TypeScript
376404 if ( document . languageId === 'typescript' ) {
377- const lines = text . split ( '\n' ) . reverse ( ) ;
378- for ( const line of lines ) {
379- const trimmedLine = line . trim ( ) . toLowerCase ( ) ;
380- if ( trimmedLine . includes ( 'actions:' ) && trimmedLine . includes ( '[' ) ) {
381- return true ;
382- }
383- if ( trimmedLine . startsWith ( '}' ) || trimmedLine . startsWith ( ']' ) ) {
405+ const allLines = document . getText ( ) . split ( '\n' ) ;
406+ const currentLineIndex = position . line ;
407+ const currentLine = allLines [ currentLineIndex ] || '' ;
408+ const currentIndent = currentLine . search ( / \S / ) ;
409+ log ( `[TS] Current line (${ currentLineIndex } ): ${ currentLine } ` ) ;
410+ log ( `[TS] Current indent: ${ currentIndent } ` ) ;
411+
412+ const actionsRegex = / a c t i o n s \s * : \s * \[ / ;
413+ for ( let i = currentLineIndex ; i >= 0 ; i -- ) {
414+ const line = allLines [ i ] ;
415+ log ( `[TS] Scanning line ${ i } : ${ line } ` ) ;
416+ if ( actionsRegex . test ( line ) ) {
417+ const match = line . match ( / ^ ( \s * ) / ) ;
418+ const parentIndent = match ? match [ 1 ] . length : 0 ;
419+ if ( currentIndent > parentIndent ) {
420+ log ( `[TS] Found actions array starting at line ${ i } , enabling autocomplete` ) ;
421+ return true ;
422+ }
384423 break ;
385424 }
386425 }
387426 }
388427
428+ // CDK Python
389429 if ( document . languageId === 'python' ) {
390- const lines = text . split ( '\n' ) . reverse ( ) ;
391- for ( const line of lines ) {
392- const trimmedLine = line . trim ( ) . toLowerCase ( ) ;
393- if ( trimmedLine . startsWith ( 'actions=' ) && trimmedLine . includes ( '[' ) ) {
394- return true ;
395- }
396- if ( trimmedLine . endsWith ( ']' ) || trimmedLine . endsWith ( '],' ) ) {
430+ const allLines = document . getText ( ) . split ( '\n' ) ;
431+ const currentLineIndex = position . line ;
432+ const currentLine = allLines [ currentLineIndex ] || '' ;
433+ const currentIndent = currentLine . search ( / \S / ) ;
434+ log ( `[Python] Current line (${ currentLineIndex } ): ${ currentLine } ` ) ;
435+ log ( `[Python] Current indent: ${ currentIndent } ` ) ;
436+
437+ const actionsRegex = / a c t i o n s \s * = \s * \[ / ;
438+ for ( let i = currentLineIndex ; i >= 0 ; i -- ) {
439+ const line = allLines [ i ] ;
440+ log ( `[Python] Scanning line ${ i } : ${ line } ` ) ;
441+ if ( actionsRegex . test ( line ) ) {
442+ const match = line . match ( / ^ ( \s * ) / ) ;
443+ const parentIndent = match ? match [ 1 ] . length : 0 ;
444+ if ( currentIndent > parentIndent ) {
445+ log ( `[Python] Found actions list starting at line ${ i } , enabling autocomplete` ) ;
446+ return true ;
447+ }
397448 break ;
398449 }
399450 }
0 commit comments