import * as assert from "assert" ; import * as vscode from "vscode" ; import { commands } from "vscode" ; import { IActionContext , IAzureQuickPickItem , IAzureUserInput } from "vscode-azureextensionui" ; import { Json , templateKeys } from "../../../extension.bundle" ; import { ext } from "../../extensionVariables" ; import { ObjectValue } from "../../language/json/JSON" ; import { ISnippet } from "../../snippets/ISnippet" ; import { KnownContexts } from "../../snippets/KnownContexts" ; import { assertNever } from '../../util/assertNever' ; import { DeploymentTemplateDoc } from "./DeploymentTemplateDoc" ; import { TemplateSectionType } from "./TemplateSectionType" ; const insertCursorText = '[]' ; const invalidTemplateMessage = `The ARM template file appears to be invalid.  If you need to, you can add contents for a new template by typing "arm!" and selecting the desired snippet.` ; export class QuickPickItem < T > implements vscode . QuickPickItem { public label : string ; public value : T ; public description : string ; constructor ( label , value , description ) { this . label = label ; this . value = value ; this . description = description ; } } type ItemType = "string" | "securestring" | "int" | "bool" | "bool" | "object" | "secureobject" | "array" ; export function getItemType ( ) { let items = [ ] ; items . push ( new QuickPickItem < ItemType > ( "String" , "string" , "A string" ) ) ; items . push ( new QuickPickItem < ItemType > ( "Secure string" , "securestring" , "A secure string" ) ) ; items . push ( new QuickPickItem < ItemType > ( "Int" , "int" , "An integer" ) ) ; items . push ( new QuickPickItem < ItemType > ( "Bool" , "bool" , "A boolean" ) ) ; items . push ( new QuickPickItem < ItemType > ( "Object" , "object" , "An object" ) ) ; items . push ( new QuickPickItem < ItemType > ( "Secure object" , "secureobject" , "A secure object" ) ) ; items . push ( new QuickPickItem < ItemType > ( "Array" , "array" , "An array" ) ) ; return items ; } async function getResourceSnippets ( ) { let items = [ ] ; const snippets = await ext . snippetManager . value . getSnippets ( KnownContexts . resources ) ; for ( const snippet of snippets ) { items . push ( { label : snippet . name , data : snippet } ) ; } return items . sort ( ( a , b ) => a . label . localeCompare ( b . label ) ) ; } export function getQuickPickItem ( label , snippet ) { return { label : label , data : snippet } ; } export function getItemTypeQuickPicks ( ) { let items = [ ] ; items . push ( new QuickPickItem < TemplateSectionType > ( "Function" , TemplateSectionType . Functions , "Insert a function" ) ) ; items . push ( new QuickPickItem < TemplateSectionType > ( "Output" , TemplateSectionType . Outputs , "Insert an output" ) ) ; items . push ( new QuickPickItem < TemplateSectionType > ( "Parameter" , TemplateSectionType . Parameters , "Insert a parameter" ) ) ; items . push ( new QuickPickItem < TemplateSectionType > ( "Resource" , TemplateSectionType . Resources , "Insert a resource" ) ) ; items . push ( new QuickPickItem < TemplateSectionType > ( "Variable" , TemplateSectionType . Variables , "Insert a variable" ) ) ; return items ; } export class InsertItem { private ui : IAzureUserInput ; constructor ( ui ) { this . ui = ui ; } public async insertItem ( template , sectionType , textEditor , context ) { if ( ! template ) { return ; } switch ( sectionType ) { case TemplateSectionType . Functions : await this . insertFunction ( template . topLevelValue , textEditor , context ) ; vscode . window . showInformationMessage ( "Please type the output of the function." ) ; break ; case TemplateSectionType . Outputs : await this . insertOutput ( template . topLevelValue , textEditor , context ) ; vscode . window . showInformationMessage ( "Please type the value of the output." ) ; break ; case TemplateSectionType . Parameters : await this . insertParameter ( template . topLevelValue , textEditor , context ) ; vscode . window . showInformationMessage ( "Done inserting parameter." ) ; break ; case TemplateSectionType . Resources : await this . insertResource ( template . topLevelValue , textEditor , context ) ; vscode . window . showInformationMessage ( "Press TAB to move between the tab stops." ) ; break ; case TemplateSectionType . Variables : await this . insertVariable ( template . topLevelValue , textEditor , context ) ; vscode . window . showInformationMessage ( "Please type the value of the variable." ) ; break ; case TemplateSectionType . TopLevel : assert . fail ( "Unknown insert item type!" ) ; default : assertNever ( sectionType ) ; } } private getTemplateObjectPart ( templateTopLevel , templatePart ) { return this . getTemplatePart ( templateTopLevel , templatePart ) ?. asObjectValue ; } private getTemplateArrayPart ( templateTopLevel , templatePart ) { return this . getTemplatePart ( templateTopLevel , templatePart ) ?. asArrayValue ; } private getTemplatePart ( templateTopLevel , templatePart ) { return templateTopLevel ?. getPropertyValue ( templatePart ) ; } public async insertParameterWithDefaultValue ( templateTopLevel , textEditor , context , name , value , description , options ? ) { let parameter = { type : "string" , defaultValue : value } ; if ( description ) { parameter . metadata = { description : description } ; } await this . insertInObject ( { templateTopLevel , textEditor , part : templateKeys . parameters , data : parameter , name , context , setCursor : false , reveal : false , options : options } ) ; return name ; } private async insertParameter ( templateTopLevel , textEditor , context ) { let name = await this . ui . showInputBox ( { prompt : "Name of parameter?" } ) ; const parameterType = await this . ui . showQuickPick ( getItemType ( ) , { placeHolder : 'Type of parameter?' } ) ; let parameter = { type : parameterType . value } ; let defaultValue = await this . ui . showInputBox ( { prompt : "Default value? Leave empty for no default value." , } ) ; if ( defaultValue ) { parameter . defaultValue = this . getDefaultValue ( defaultValue , parameterType . value , context ) ; } let description = await this . ui . showInputBox ( { prompt : "Description? Leave empty for no description." , } ) ; if ( description ) { parameter . metadata = { description : description } ; } await this . insertInObject ( { templateTopLevel , textEditor , part : templateKeys . parameters , data : parameter , name , context , setCursor : false } ) ; } private throwExpectedError ( message , context , suppressReportIssue = true ) { context . errorHandling . suppressReportIssue = suppressReportIssue ; throw new Error ( message ) ; } private getDefaultValue ( defaultValue , parameterType , context ) { switch ( parameterType ) { case "int" : let intValue = Number ( defaultValue ) ; if ( isNaN ( intValue ) ) { this . throwExpectedError ( "Please enter a valid integer value" , context ) ; } return intValue ; case "array" : try { return JSON . parse ( defaultValue ) ; } catch ( error ) { this . throwExpectedError ( "Please enter a valid array value" , context ) ; } break ; case "object" : case "secureobject" : try { return JSON . parse ( defaultValue ) ; } catch ( error ) { this . throwExpectedError ( "Please enter a valid object value" , context ) ; } break ; case "bool" : switch ( defaultValue ) { case "true" : return true ; case "false" : return false ; default : this . throwExpectedError ( "Please enter 'true' or 'false'" , context ) ; } break ; case "string" : case "securestring" : return defaultValue ; default : assertNever ( parameterType ) ; } } public async insertInObject ( { templateTopLevel , textEditor , part , data , name , context , setCursor = true , reveal = true , options = { undoStopBefore : true , undoStopAfter : true } } ) { let templatePart = this . getTemplateObjectPart ( templateTopLevel , part ) ; if ( ! templatePart ) { if ( ! templateTopLevel ) { context . errorHandling . suppressReportIssue = true ; throw new Error ( invalidTemplateMessage ) ; } let subPart = { } ; subPart [ name ] = data ; await this . insertInObjectHelper ( { templatePart : templateTopLevel , textEditor , data : subPart , name : part , setCursor , reveal , options } ) ; } else { await this . insertInObjectHelper ( { templatePart , textEditor , data , name , setCursor , reveal , options } ) ; } } returns private async insertInObjectHelper ( { templatePart , textEditor , data , name , setCursor = true , reveal = true , options = { undoStopBefore : true , undoStopAfter : true } } ) { let isFirstItem = templatePart . properties . length === 0 ; let startText = isFirstItem ? '' : ',' ; let templatePartStartPos = textEditor . document . positionAt ( templatePart . span . startIndex ) ; let templatePartStartLine = textEditor . document . lineAt ( templatePartStartPos . line ) ; let templatePartInitialWhitespace = templatePartStartLine . text . match ( /^\s*/ ) ! [ 0 ] ; let spacesPerTab = Math . max ( 1 , Number ( textEditor . options . tabSize ) ?? 1 ) ; let templatePartIndentColumn = templatePartInitialWhitespace . replace ( /\t/g , ' ' . repeat ( spacesPerTab ) ) . length ; spacesPerTab = Number . isSafeInteger ( spacesPerTab ) ? spacesPerTab : 1 ; let templatePartIndentLevel = Math . max ( 0 , Math . floor ( templatePartIndentColumn / spacesPerTab ) ) ; let textIndentLevel = templatePartIndentLevel + 1 ; let insertIndex = isFirstItem ? templatePart . span . endIndex : templatePart . properties [ templatePart . properties . length - 1 ] . span . afterEndIndex ; let endTabs = '\t' . repeat ( textIndentLevel - 1 ) ; let endText = isFirstItem ? `\r\n${ endTabs }` : `` ; let text = typeof ( data ) === 'object' ? JSON . stringify ( data , null , '\t' ) : `"${ data }"` ; let indentedText = this . indent ( `\r\n"${ name }": ${ text }` , textIndentLevel ) ; return await this . insertText ( textEditor , insertIndex , `${ startText }${ indentedText }${ endText }` , setCursor , reveal , options ) ; } private async insertVariable ( templateTopLevel , textEditor , context ) { let name = await this . ui . showInputBox ( { prompt : "Name of variable?" } ) ; await this . insertInObject ( { templateTopLevel , textEditor , part : templateKeys . variables , data : insertCursorText , name , context } ) ; } public async insertVariableWithValue ( templateTopLevel , textEditor , context , name , value , options ? ) { await this . insertInObject ( { templateTopLevel , textEditor , part : templateKeys . variables , data : value , name , context , setCursor : false , reveal : false , options : options } ) ; } private async insertOutput ( templateTopLevel , textEditor , context ) { let name = await this . ui . showInputBox ( { prompt : "Name of output?" } ) ; const outputType = await this . ui . showQuickPick ( getItemType ( ) , { placeHolder : 'Type of output?' } ) ; let output = { type : outputType . value , value : insertCursorText . replace ( /"/g , '' ) } ; await this . insertInObject ( { templateTopLevel , textEditor , part : templateKeys . outputs , data : output , name , context } ) ; } private async insertFunctionAsTopLevel ( topLevel , textEditor , context ) { if ( ! topLevel ) { context . errorHandling . suppressReportIssue = true ; throw new Error ( invalidTemplateMessage ) ; } let functions = [ await this . getFunctionNamespace ( ) ] ; await this . insertInObjectHelper ( { templatePart : topLevel , textEditor , data : functions , name : templateKeys . functions , } ) ; } private async insertFunctionAsNamespace ( functions , textEditor ) { let namespace = await this . getFunctionNamespace ( ) ; await this . insertInArray ( functions , textEditor , namespace ) ; } private async insertFunctionAsMembers ( namespace , textEditor ) { let functionName = await this . ui . showInputBox ( { prompt : "Name of function?" } ) ; let functionDef = await this . getFunction ( ) ; let members = { } ; members [ functionName ] = functionDef ; await this . insertInObjectHelper ( { templatePart : namespace , textEditor , data : members , name : templateKeys . userFunctionMembers , } ) ; } private async insertFunctionAsFunction ( members , textEditor ) { let functionName = await this . ui . showInputBox ( { prompt : "Name of function?" } ) ; let functionDef = await this . getFunction ( ) ; await this . insertInObjectHelper ( { templatePart : members , textEditor , data : functionDef , name : functionName , } ) ; } private async insertFunction ( templateTopLevel , textEditor , context ) { let functions = this . getTemplateArrayPart ( templateTopLevel , templateKeys . functions ) ; if ( ! functions ) { await this . insertFunctionAsTopLevel ( templateTopLevel , textEditor , context ) ; return ; } if ( functions . length === 0 ) { await this . insertFunctionAsNamespace ( functions , textEditor ) ; return ; } let namespace = Json . asObjectValue ( functions . elements [ 0 ] ) ; if ( ! namespace ) { context . errorHandling . suppressReportIssue = true ; throw new Error ( `The first namespace in "functions" is not an object` ) ; } let members = namespace . getPropertyValue ( templateKeys . userFunctionMembers ) ; if ( ! members ) { await this . insertFunctionAsMembers ( namespace , textEditor ) ; return ; } let membersObject = Json . asObjectValue ( members ) ; if ( ! membersObject ) { context . errorHandling . suppressReportIssue = true ; throw new Error ( `The first namespace in "functions" does not have members as an object!` ) ; } await this . insertFunctionAsFunction ( membersObject , textEditor ) ; return ; } private async insertResource ( templateTopLevel , textEditor , context ) { let resources = this . getTemplateArrayPart ( templateTopLevel , templateKeys . resources ) ; let index ; let prepend = "\r\n\t\t\r\n\t" ; if ( ! resources ) { if ( ! templateTopLevel ) { context . errorHandling . suppressReportIssue = true ; throw new Error ( invalidTemplateMessage ) ; } let subPart = [ ] ; index = await this . insertInObjectHelper ( { templatePart : templateTopLevel , textEditor , data : subPart , name : templateKeys . resources , } ) ; } else { index = resources . span . endIndex ; if ( resources . elements . length > 0 ) { let lastIndex = resources . elements . length - 1 ; index = resources . elements [ lastIndex ] . span . afterEndIndex ; prepend = `,\r\n\t\t` ; } } const resource = await this . ui . showQuickPick ( await getResourceSnippets ( ) , { placeHolder : 'What resource do you want to insert?' } ) ; await this . insertText ( textEditor , index , prepend ) ; let newCursorPosition = this . getCursorPositionForInsertResource ( textEditor , index , prepend ) ; textEditor . selection = new vscode . Selection ( newCursorPosition , newCursorPosition ) ; await commands . executeCommand ( 'editor.action.insertSnippet' , { snippet : resource . data . insertText } ) ; textEditor . revealRange ( new vscode . Range ( newCursorPosition , newCursorPosition ) , vscode . TextEditorRevealType . AtTop ) ; } private getCursorPositionForInsertResource ( textEditor , index , prepend ) { let prependRange = new vscode . Range ( textEditor . document . positionAt ( index ) , textEditor . document . positionAt ( index + this . formatText ( prepend , textEditor ) . length ) ) ; let prependFromDocument = textEditor . document . getText ( prependRange ) ; let lookFor = this . formatText ( '\t\t' , textEditor ) ; let cursorPos = prependFromDocument . indexOf ( lookFor ) ; return textEditor . document . positionAt ( index + cursorPos + lookFor . length ) ; } private async getFunction ( ) { const outputType = await this . ui . showQuickPick ( getItemType ( ) , { placeHolder : 'Type of function output?' } ) ; let parameters = await this . getFunctionParameters ( ) ; let functionDef = { parameters : parameters , output : { type : outputType . value , value : insertCursorText } } ; return functionDef ; } private async getFunctionParameters ( ) { let parameterName ; let parameters = [ ] ; do { const msg = parameters . length === 0 ? "Name of first parameter?" : "Name of next parameter?" ; const leaveEmpty = "Press 'Enter' if there are no more parameters." ; parameterName = await this . ui . showInputBox ( { prompt : msg , placeHolder : leaveEmpty } ) ; if ( parameterName !== '' ) { const parameterType = await this . ui . showQuickPick ( getItemType ( ) , { placeHolder : `Type of parameter ${ parameterName }?` } ) ; parameters . push ( { name : parameterName , type : parameterType . value } ) ; } } while ( parameterName !== '' ) ; return parameters ; } private async insertInArray ( templatePart , textEditor , data ) { let index = templatePart . span . endIndex ; let text = JSON . stringify ( data , null , '\t' ) ; let indentedText = this . indent ( `\r\n${ text }\r\n` , 2 ) ; await this . insertText ( textEditor , index , `${ indentedText }\t` ) ; } private async getFunctionNamespace ( ) { let namespaceName = await this . ui . showInputBox ( { prompt : "Name of namespace?" } ) ; let namespace = { namespace : namespaceName , members : await this . getFunctionMembers ( ) } ; return namespace ; } private async getFunctionMembers ( ) { let functionName = await this . ui . showInputBox ( { prompt : "Name of function?" } ) ; let functionDef = await this . getFunction ( ) ; let members = { } ; members [ functionName ] = functionDef ; return members ; } param textEditor param index param text param setCursor param options returns private async insertText ( textEditor , index , text , setCursor = true , reveal = true , options ? ) { text = this . formatText ( text , textEditor ) ; let pos = textEditor . document . positionAt ( index ) ; await textEditor . edit ( builder => builder . insert ( pos , text ) , options ) ; if ( reveal ) { let endPos = textEditor . document . positionAt ( index + text . length ) ; textEditor . revealRange ( new vscode . Range ( pos , endPos ) , vscode . TextEditorRevealType . Default ) ; } if ( setCursor && text . lastIndexOf ( insertCursorText ) >= 0 ) { let insertedText = textEditor . document . getText ( new vscode . Range ( pos , textEditor . document . positionAt ( index + text . length ) ) ) ; let cursorPos = insertedText . lastIndexOf ( insertCursorText ) ; let newIndex = index + cursorPos + insertCursorText . length / 2 ; let pos2 = textEditor . document . positionAt ( newIndex ) ; textEditor . selection = new vscode . Selection ( pos2 , pos2 ) ; return newIndex ; } return 0 ; } private formatText ( text , textEditor ) { if ( textEditor . options . insertSpaces === true ) { text = text . replace ( /\t/g , ' ' . repeat ( Number ( textEditor . options . tabSize ) ) ) ; } else { text = text . replace ( / {4}/g , '\t' ) ; } return text ; } param str param numOfTabs return private indent ( str , numOfTabs ) { str = str . replace ( /^(?=.)/gm , '\t' . repeat ( numOfTabs ) ) ; return str ; } } interface ParameterMetaData { description : string ; } interface Parameter extends Data { type : string ; defaultValue ? : string | number | boolean | unknown | { } | [ ] ; metadata ? : ParameterMetaData ; } interface Output extends Data { type : string ; value : string ; } interface Function extends Data { parameters : Parameter [ ] ; output : Output ; } interface FunctionParameter extends Data { name : string ; type : string ; } interface FunctionNameSpace { namespace : string ; members : any [ ] ; } type Data = { [ key ] : unknown ; } ;