11import { CompletionItemProvider , TextDocument , Position , CancellationToken , CompletionItem , CompletionItemKind } from "vscode" ;
2- import resources from '../../config/tips/tiat-resources.json' ;
2+ // import resources from '../../config/tips/tiat-resources.json';
33import * as _ from "lodash" ;
44import * as vscode from 'vscode' ;
5-
5+ import { executeCommandByExec } from "@/utils/cpUtils" ;
6+ import * as fs from "fs" ;
7+ import * as path from "path" ;
8+ import * as workspaceUtils from "@/utils/workspaceUtils" ;
9+ import * as TelemetryWrapper from "vscode-extension-telemetry-wrapper" ;
10+
11+ const LATEST_VERSION = "latest" ;
12+ const versionPattern = / ^ v \d + ( \. \d + ) { 2 } \. j s o n $ / ;
613let topLevelTypes = [ "output" , "provider" , "resource" , "variable" , "data" ] ;
714let topLevelRegexes = topLevelTypes . map ( o => {
815 return {
@@ -15,6 +22,29 @@ interface TerraformCompletionContext extends vscode.CompletionContext {
1522 resourceType ?: string ;
1623}
1724
25+ interface Argument {
26+ name : string ;
27+ description : string ;
28+ options ?: Array < string > ;
29+ detail ?: Array < Argument > ;
30+ }
31+
32+ interface Attribute {
33+ name : string ;
34+ description : string ;
35+ detail ?: Array < Attribute > ;
36+ }
37+
38+ interface Tips {
39+ version : string ;
40+ resource : {
41+ [ key : string ] : {
42+ args : Array < Argument > ;
43+ attrs : Array < Attribute > ;
44+ } ;
45+ } ;
46+ }
47+
1848const TEXT_MIN_SORT = "a" ;
1949const TEXT_FILTER = " " ;
2050
@@ -26,8 +56,12 @@ export class TerraformTipsProvider implements CompletionItemProvider {
2656 position : Position ;
2757 token : CancellationToken ;
2858 resourceType : string | null = null ;
59+ private extensionPath : string ;
60+ constructor ( extensionPath : string ) {
61+ this . extensionPath = extensionPath ;
62+ }
2963
30- public provideCompletionItems ( document : TextDocument , position : Position , token : CancellationToken , context : TerraformCompletionContext ) : CompletionItem [ ] {
64+ public async provideCompletionItems ( document : TextDocument , position : Position , token : CancellationToken , context : TerraformCompletionContext ) : Promise < CompletionItem [ ] > {
3165 this . document = document ;
3266 this . position = position ;
3367 this . token = token ;
@@ -36,7 +70,7 @@ export class TerraformTipsProvider implements CompletionItemProvider {
3670 const lineText = document . lineAt ( position . line ) . text ;
3771 const lineTillCurrentPosition = lineText . substring ( 0 , position . character ) ;
3872
39- // Are we trying to type a top type?
73+ // handle top level definition
4074 if ( this . isTopLevelType ( lineTillCurrentPosition ) ) {
4175 return this . getTopLevelType ( lineTillCurrentPosition ) ;
4276 }
@@ -76,24 +110,32 @@ export class TerraformTipsProvider implements CompletionItemProvider {
76110 // We're trying to type the exported field for the let
77111 const resourceType = parts [ 0 ] ;
78112 let resourceName = parts [ 1 ] ;
79- let attrs = resources [ resourceType ] . attrs ;
80- let result = _ . map ( attrs , o => {
81- let c = new CompletionItem ( `${ o . name } (${ resourceType } )` , CompletionItemKind . Property ) ;
82- c . detail = o . description ;
83- c . insertText = o . name ;
84- c . sortText = TEXT_MIN_SORT ;
85- return c ;
86- } ) ;
87- return result ;
113+ try {
114+ // async load resource config
115+ const tips = await loadResource ( this . extensionPath ) ;
116+ const resources = tips . resource ;
117+ let attrs = resources [ resourceType ] . attrs ;
118+ let result = _ . map ( attrs , o => {
119+ let c = new CompletionItem ( `${ o . name } (${ resourceType } )` , CompletionItemKind . Property ) ;
120+ c . detail = o . description ;
121+ c . insertText = o . name ;
122+ c . sortText = TEXT_MIN_SORT ;
123+ return c ;
124+ } ) ;
125+ return result ;
126+
127+ } catch ( error ) {
128+ console . error ( `Can not load resource from json. error:[${ error } ]` ) ;
129+ }
88130 }
89131
90132 // Which part are we completing for?
91133 return [ ] ;
92134 }
93135
94136 // Are we trying to type a parameter to a resource?
95- let possibleResources = this . checkTopLevelResource ( lineTillCurrentPosition ) ;
96- // typing a resource type
137+ let possibleResources = await this . checkTopLevelResource ( lineTillCurrentPosition ) ;
138+ // handle resource type
97139 if ( possibleResources . length > 0 ) {
98140 return this . getHintsForStrings ( possibleResources ) ;
99141 }
@@ -106,28 +148,43 @@ export class TerraformTipsProvider implements CompletionItemProvider {
106148 if ( endwithEqual ) {
107149 const lineBeforeEqualSign = lineTillCurrentPosition . substring ( 0 , includeEqual ) . trim ( ) ;
108150 // load options
109- const name = lineBeforeEqualSign ;
110- const argStrs = this . findArgByName ( resources [ this . resourceType ] . args , name ) ;
111- const options = this . getOptionsFormArg ( argStrs ) ;
112- // clear resource type
113- this . resourceType = "" ;
114- return ( options ) . length ? options : [ ] ;
151+ try {
152+ // async load resource config
153+ const tips = await loadResource ( this . extensionPath ) ;
154+ const name = lineBeforeEqualSign ;
155+ const resources = tips . resource ;
156+ const argStrs = this . findArgByName ( resources [ this . resourceType ] . args , name ) ;
157+ const options = this . getOptionsFormArg ( argStrs ) ;
158+ // clear resource type
159+ this . resourceType = "" ;
160+ return ( options ) . length ? options : [ ] ;
161+ } catch ( error ) {
162+ console . error ( `Can not load resource from json when loading options. error:[${ error } ]` ) ;
163+ }
115164 }
116165 this . resourceType = "" ;
117166 return [ ] ;
118167 }
119168
120- // Check if we're in a resource definition
169+ // handle argument
121170 if ( includeEqual < 0 && ! endwithEqual ) {
122171 // we're not in options case
123172 for ( let i = position . line - 1 ; i >= 0 ; i -- ) {
124173 let line = document . lineAt ( i ) . text ;
125174 let parentType = this . getParentType ( line ) ;
126175 if ( parentType && parentType . type === "resource" ) {
127- // typing a arg in resource
176+ // typing a argument in resource
128177 const resourceType = this . getResourceTypeFromLine ( line ) ;
129- const ret = this . getItemsForArgs ( resources [ resourceType ] . args , resourceType ) ;
130- return ret ;
178+ try {
179+ // async load resource config
180+ const tips = await loadResource ( this . extensionPath ) ;
181+ const resources = tips . resource ;
182+ const ret = this . getItemsForArgs ( resources [ resourceType ] . args , resourceType ) ;
183+ return ret ;
184+ } catch ( error ) {
185+ console . error ( `Can not load resource from json when loading argument. error:[${ error } ]` ) ;
186+ return [ ] ;
187+ }
131188 }
132189 else if ( parentType && parentType . type !== "resource" ) {
133190 // We don't want to accidentally include some other containers stuff
@@ -237,18 +294,27 @@ export class TerraformTipsProvider implements CompletionItemProvider {
237294 return "" ;
238295 }
239296
240- checkTopLevelResource ( lineTillCurrentPosition : string ) : any [ ] {
297+ async checkTopLevelResource ( lineTillCurrentPosition : string ) : Promise < any [ ] > {
241298 let parts = lineTillCurrentPosition . split ( " " ) ;
242299 if ( parts . length === 2 && parts [ 0 ] === "resource" ) {
243300 let r = parts [ 1 ] . replace ( / " / g, '' ) ;
244301 let regex = new RegExp ( "^" + r ) ;
245- let possibleResources = _ . filter ( _ . keys ( resources ) , k => {
246- if ( regex . test ( k ) ) {
247- return true ;
248- }
249- return false ;
250- } ) ;
251- return possibleResources ;
302+ // handle resource
303+ try {
304+ // async load resource config
305+ const tips = await loadResource ( this . extensionPath ) ;
306+ const resources = tips . resource ;
307+ let possibleResources = _ . filter ( _ . keys ( resources ) , k => {
308+ if ( regex . test ( k ) ) {
309+ return true ;
310+ }
311+ return false ;
312+ } ) ;
313+ return possibleResources ;
314+ } catch ( error ) {
315+ console . error ( `Can not load resource from json when loading resource type. error:[${ error } ]` ) ;
316+ return [ ] ;
317+ }
252318 }
253319 return [ ] ;
254320 }
@@ -295,7 +361,7 @@ export class TerraformTipsProvider implements CompletionItemProvider {
295361 }
296362
297363 const changes = event . contentChanges [ 0 ] ;
298- if ( changes . text === TIPS_OPTIONS_TRIGGER_CHARACTER ) {
364+ if ( changes && changes . text === TIPS_OPTIONS_TRIGGER_CHARACTER ) {
299365 const position = activeEditor . selection . active ;
300366 const resourceType = this . findResourceType ( event . document , position ) ;
301367
@@ -305,4 +371,94 @@ export class TerraformTipsProvider implements CompletionItemProvider {
305371 }
306372 }
307373 }
308- }
374+ }
375+
376+ async function sortJsonFiles ( dir : string ) {
377+ let jsonFiles : string [ ] ;
378+ try {
379+ const files = fs . readdirSync ( dir ) ;
380+ jsonFiles = files . filter ( file => path . extname ( file ) === '.json' && versionPattern . test ( file ) ) ;
381+ // const jsonFiles: string[] = ["v1.81.50.json", "v1.81.54.json"]; // debug data
382+ } catch ( error ) {
383+ console . error ( `read dir failed. error:[${ error } ]` ) ;
384+ return null ;
385+ }
386+
387+ // import files
388+ const versions = await Promise . all ( jsonFiles . map ( async file => {
389+ const jsonPath = path . join ( "../config/tips/" , file ) ;
390+ // const json = await import(jsonPath);
391+ const json = require ( jsonPath ) ;
392+ const version = json . version as string ;
393+ return {
394+ json,
395+ version
396+ } ;
397+ } ) ) ;
398+
399+ // sort with version desc
400+ versions . sort ( ( a , b ) => compareVersions ( b . version , a . version ) ) ;
401+ return versions ;
402+ }
403+
404+ function compareVersions ( a , b ) {
405+ if ( a && ! b ) { return 1 ; }
406+ if ( ! a && b ) { return - 1 ; }
407+ if ( a === 'latest' ) { return 1 ; }
408+ if ( b === 'latest' ) { return - 1 ; }
409+ const aParts = a . split ( '.' ) . map ( Number ) ;
410+ const bParts = b . split ( '.' ) . map ( Number ) ;
411+
412+ for ( let i = 0 ; i < aParts . length ; i ++ ) {
413+ if ( aParts [ i ] > bParts [ i ] ) {
414+ return 1 ;
415+ } else if ( aParts [ i ] < bParts [ i ] ) {
416+ return - 1 ;
417+ }
418+ }
419+ //equal
420+ return 0 ;
421+ }
422+
423+ // load resource config from json files based on the appropriate version
424+ async function loadResource ( extPath : string ) : Promise < Tips > {
425+ let tfVersion : string ;
426+ const cwd = workspaceUtils . getActiveEditorPath ( ) ;
427+ if ( ! cwd ) {
428+ TelemetryWrapper . sendError ( Error ( "noWorkspaceSelected" ) ) ;
429+ console . error ( `can not get path from active editor` ) ;
430+ }
431+
432+ await executeCommandByExec ( "terraform version" , cwd ) . then ( output => {
433+ let match = RegExp ( / t e n c e n t c l o u d s t a c k \/ t e n c e n t c l o u d ( v \d + \. \d + \. \d + ) / ) . exec ( output ) ;
434+
435+ if ( match ) {
436+ tfVersion = match [ 1 ] ;
437+ } else {
438+ // gives the latest JSON if not tf provider version found
439+ tfVersion = LATEST_VERSION ;
440+ }
441+ console . log ( `tf provider version:[${ tfVersion } ], cwd:[${ cwd } ]` ) ;
442+ } ) . catch ( error => {
443+ console . error ( `execute terraform version failed: ${ error } ` ) ;
444+ } ) ;
445+
446+ let result : Tips | null = null ;
447+ const tipsDir = path . join ( extPath , 'config' , 'tips' ) ;
448+ const tipFiles = await sortJsonFiles ( tipsDir ) ;
449+
450+ tipFiles . some ( file => {
451+ if ( compareVersions ( tfVersion , file . version ) >= 0 ) {
452+ result = file . json as Tips ;
453+ return true ;
454+ }
455+ // gives the latest JSON if not one JSON files matched
456+ result = file . json as Tips ;
457+ return false ;
458+ } ) ;
459+
460+ console . log ( `Loaded json. tf version:[${ tfVersion } ], json version:[${ result . version } ]` ) ;
461+ // vscode.window.showInformationMessage(`Loaded json. tf version:[${tfVersion}], json version:[${result.version}]`);
462+
463+ return result ;
464+ }
0 commit comments