import * as assert from "assert" ; import { CodeAction , CodeActionContext , CodeActionKind , Range , Selection , TextEditor } from "vscode" ; import { Command } from "vscode-languageclient" ; import { Completion , INamedDefinition , Json } from "../../../extension.bundle" ; import { ext } from "../../extensionVariables" ; import { Issue } from "../../language/Issue" ; import { IssueKind } from "../../language/IssueKind" ; import { ReferenceList } from "../../language/ReferenceList" ; import { ContainsBehavior , Span } from "../../language/Span" ; import { indentMultilineString } from "../../util/multilineStrings" ; import { IAddMissingParametersArgs } from "../../vscodeIntegration/commandArguments" ; import { getVSCodePositionFromPosition , getVSCodeRangeFromSpan } from "../../vscodeIntegration/vscodePosition" ; import { IReferenceSite , ReferenceSiteKind } from "../positionContexts/PositionContext" ; import { IJsonDocument } from "../templates/IJsonDocument" ; import { TemplateScope } from "../templates/scopes/TemplateScope" ; import { IParameterDefinition } from "./IParameterDefinition" ; import { IParameterDefinitionsSource } from "./IParameterDefinitionsSource" ; import { IParameterValuesSource } from "./IParameterValuesSource" ; import { createParameterFromTemplateParameter , defaultTabSize } from "./parameterFileGeneration" ; const EOL = ext . EOL ; const newParameterValueSnippetLabel = `new-parameter-value` ; /**
 * Code actions for adding missing parameter values
 */ export function getParameterValuesCodeActions ( parameterValuesSource , parameterDefinitionsSource , parentParameterDefinitionsSource , range , context ) { const actions = [ ] ; const parametersProperty = parameterValuesSource . parameterValuesProperty ; if ( parametersProperty ) { const lineIndexOfParametersProperty = parameterValuesSource . document . getDocumentPosition ( parametersProperty . nameValue . span . startIndex ) . line ; if ( lineIndexOfParametersProperty >= range . start . line && lineIndexOfParametersProperty <= range . end . line ) { const missingParameters = getMissingParameters ( parameterDefinitionsSource , parameterValuesSource , false ) ; if ( missingParameters . some ( isParameterRequired ) ) { const action = new CodeAction ( "Add missing required parameters" , CodeActionKind . QuickFix ) ; action . command = { command : 'azurerm-vscode-tools.codeAction.addMissingRequiredParameters' , title : action . title , arguments : [ parameterValuesSource . document . documentUri , < IAddMissingParametersArgs > { parameterValuesSource , parameterDefinitionsSource , parentParameterDefinitionsSource } ] } ; actions . push ( action ) ; } if ( missingParameters . length > 0 ) { const action = new CodeAction ( "Add all missing parameters" , CodeActionKind . QuickFix ) ; action . command = { command : 'azurerm-vscode-tools.codeAction.addAllMissingParameters' , title : action . title , arguments : [ parameterValuesSource . document . documentUri , < IAddMissingParametersArgs > { parameterValuesSource , parameterDefinitionsSource , parentParameterDefinitionsSource } ] } ; actions . push ( action ) ; } } } return actions ; } function isParameterRequired ( paramDef ) { return ! paramDef . defaultValue ; } export function getMissingParameters ( parameterDefinitionsSource , parameterValuesSource , onlyRequiredParameters ) { const results = [ ] ; for ( let paramDef of parameterDefinitionsSource ?. parameterDefinitions ?? [ ] ) { const paramValue = parameterValuesSource . getParameterValue ( paramDef . nameValue . unquotedValue ) ; if ( ! paramValue ) { results . push ( paramDef ) ; } } if ( onlyRequiredParameters ) { return results . filter ( isParameterRequired ) ; } return results ; } export async function addMissingParameters ( parameterDefinitionsSource , parameterValuesSource , parameterValuesSourceEditor , parentParameterDefinitions , onlyRequiredParameters ) { const parameterValuesDocument = parameterValuesSource . document ; const parametersObjectValue = parameterValuesSource . parameterValuesProperty ?. value ?. asObjectValue ; if ( parameterValuesSource . parameterValuesProperty && parametersObjectValue ) { const parametersProperty = parameterValuesSource . parameterValuesProperty ; let lastTokenInParameters ; for ( let i = parametersProperty . span . endIndex - 1 ; i >= parametersProperty . span . startIndex ; -- i ) { lastTokenInParameters = parameterValuesDocument . jsonParseResult . getTokenAtCharacterIndex ( i , Json . Comments . includeCommentTokens ) ; if ( lastTokenInParameters ) { break ; } } const insertIndex = lastTokenInParameters ? lastTokenInParameters . span . afterEndIndex : parametersObjectValue . span . endIndex ; const insertPosition = parameterValuesDocument . getDocumentPosition ( insertIndex ) ; const missingParams = getMissingParameters ( parameterDefinitionsSource , parameterValuesSource , onlyRequiredParameters ) ; if ( missingParams . length === 0 ) { return ; } let paramsAsText = [ ] ; for ( let param of missingParams ) { const paramText = createParameterFromTemplateParameter ( parameterDefinitionsSource , param , parentParameterDefinitions , defaultTabSize ) ; paramsAsText . push ( paramText ) ; } let newText = paramsAsText . join ( `,${ EOL }` ) ; const parametersObjectIndent = parameterValuesDocument . getDocumentPosition ( parametersProperty ?. nameValue . span . startIndex ) . column ; const lastParameter = parameterValuesSource . parameterValueDefinitions . length > 0 ? parameterValuesSource . parameterValueDefinitions [ parameterValuesSource . parameterValueDefinitions . length - 1 ] : undefined ; const lastParameterIndent = lastParameter ? parameterValuesDocument . getDocumentPosition ( lastParameter ?. fullSpan . startIndex ) . column : undefined ; const newTextIndent = lastParameterIndent === undefined ? parametersObjectIndent + defaultTabSize : lastParameterIndent ; let indentedText = indentMultilineString ( newText , newTextIndent ) ; let insertText = EOL + indentedText ; if ( parameterValuesDocument . getDocumentPosition ( insertIndex ) . line === parameterValuesDocument . getDocumentPosition ( parametersObjectValue . span . endIndex ) . line ) { insertText += EOL + ' ' . repeat ( defaultTabSize ) ; } let commaEdit = createEditToAddCommaBeforePosition ( parameterValuesSource , insertIndex ) ; assert ( ! commaEdit || commaEdit . span . endIndex <= insertIndex ) ; if ( commaEdit ?. span . startIndex === insertIndex ) { commaEdit = undefined ; insertText = `,${ insertText }` ; } await parameterValuesSourceEditor . edit ( editBuilder => { editBuilder . insert ( getVSCodePositionFromPosition ( insertPosition ) , insertText ) ; if ( commaEdit ) { editBuilder . replace ( getVSCodeRangeFromSpan ( parameterValuesDocument , commaEdit . span ) , commaEdit . insertText ) ; } } ) ; } } export function createEditToAddCommaBeforePosition ( parameterValuesSource , documentIndex ) { const newParamIndex = parameterValuesSource . parameterValueDefinitions . filter ( p => p . fullSpan . endIndex < documentIndex ) . length ; if ( newParamIndex > 0 ) { const prevParameter = parameterValuesSource . parameterValueDefinitions [ newParamIndex - 1 ] ; assert ( prevParameter ) ; const firstIndexAfterPrev = prevParameter . fullSpan . afterEndIndex ; const tokensBetweenParams = parameterValuesSource . document . jsonParseResult . getTokensInSpan ( new Span ( firstIndexAfterPrev , documentIndex - firstIndexAfterPrev ) , Json . Comments . ignoreCommentTokens ) ; if ( tokensBetweenParams . some ( t => t . type === Json . TokenType . Comma ) ) { return undefined ; } const insertIndex = prevParameter . fullSpan . afterEndIndex ; return { insertText : ',' , span : new Span ( insertIndex , 0 ) } ; } return undefined ; } export function getCompletionForNewParameter ( parameterValuesSource , documentIndex ) { const detail = "Insert new parameter" ; let snippet = `"\${1:parameter1}": {` + EOL + `\t"value": "\${2:value}"` + EOL + `}` ; const documentation = "documentation" ; const label = newParameterValueSnippetLabel ; return createParameterCompletion ( parameterValuesSource , documentIndex , label , snippet , Completion . CompletionKind . PropertyValueForNewProperty , detail , documentation ) ; } /**
 * Get completion items for our position in the document
 */ export function getCompletionsForMissingParameters ( parameterDefinitionsSource , parameterValuesSource , parentParameterDefinitionsSource , tabSize , documentIndex ) { const completions = [ ] ; const paramsInParameterFile = parameterValuesSource . parameterValueDefinitions . map ( pv => pv . nameValue . unquotedValue . toLowerCase ( ) ) ; for ( let param of parameterDefinitionsSource . parameterDefinitions ) { const paramNameLC = param . nameValue . unquotedValue . toLowerCase ( ) ; if ( paramsInParameterFile . includes ( paramNameLC ) ) { continue ; } const isRequired = ! param . defaultValue ; const label = param . nameValue . quotedValue ; const paramText = createParameterFromTemplateParameter ( parameterDefinitionsSource , param , parentParameterDefinitionsSource , tabSize ) ; let replacement = paramText ; const documentation = `Insert a value for parameter "${ param . nameValue . unquotedValue }"` ; const detail = ( isRequired ? "(required parameter)" : "(optional parameter)" ) + EOL + EOL + paramText ; completions . push ( createParameterCompletion ( parameterValuesSource , documentIndex , label , replacement , Completion . CompletionKind . PropertyValueForExistingProperty , detail , documentation ) ) ; } return completions ; } function createParameterCompletion ( parameterValuesSource , documentIndex , label , insertText , kind , detail , documentation ) { const document = parameterValuesSource . document ; let token = document . getJSONTokenAtDocumentCharacterIndex ( documentIndex ) ; if ( ! token && documentIndex > 0 ) { token = document . getJSONTokenAtDocumentCharacterIndex ( documentIndex - 1 ) ; } const span = token ?. span ?? new Span ( documentIndex , 0 ) ; if ( needsCommaAfterCompletion ( parameterValuesSource , documentIndex ) ) { insertText += ',' ; } const commaEdit = createEditToAddCommaBeforePosition ( parameterValuesSource , documentIndex ) ; if ( token ?. type === Json . TokenType . QuotedString ) { if ( label [ 0 ] !== '"' ) { label = `"${ label }"` ; } } return new Completion . Item ( { label , insertText , span , kind , detail , documentation , additionalEdits : commaEdit ? [ commaEdit ] : undefined } ) ; } function needsCommaAfterCompletion ( parameterValuesSource , documentCharacterIndex ) { if ( parameterValuesSource . parameterValueDefinitions . some ( p => p . fullSpan . startIndex >= documentCharacterIndex ) ) { return true ; } return false ; } export function getPropertyValueCompletionItems ( parameterDefinitionsSource , parameterValuesSource , parentParameterDefinitionsSource , tabSize , documentIndex , triggerCharacter ) { let completions = [ ] ; if ( ( ! triggerCharacter || triggerCharacter === '"' ) && canAddPropertyValueHere ( parameterValuesSource , documentIndex ) ) { if ( parameterDefinitionsSource ) { completions . push ( ... getCompletionsForMissingParameters ( parameterDefinitionsSource , parameterValuesSource , parentParameterDefinitionsSource , tabSize , documentIndex ) ) ; } completions . push ( getCompletionForNewParameter ( parameterValuesSource , documentIndex ) ) ; } return completions ; } export function canAddPropertyValueHere ( parameterValuesSource , documentIndex ) { const parametersObjectValue = parameterValuesSource . parameterValuesProperty ?. value ?. asObjectValue ; if ( ! parametersObjectValue ) { return false ; } const enclosingJsonValue = parameterValuesSource . document . jsonParseResult . getValueAtCharacterIndex ( documentIndex , ContainsBehavior . enclosed ) ; if ( enclosingJsonValue !== parametersObjectValue ) { return false ; } if ( ! ! parameterValuesSource . document . jsonParseResult . getCommentTokenAtDocumentIndex ( documentIndex , ContainsBehavior . enclosed ) ) { return false ; } return true ; } export function getMissingParameterErrors ( parameterValues , parameterDefinitions ) { const missingRequiredParams = getMissingParameters ( parameterDefinitions , parameterValues , true ) ; if ( missingRequiredParams . length === 0 ) { return [ ] ; } const missingParamNames = missingRequiredParams . map ( param => `"${ param . nameValue . unquotedValue }"` ) ; const message = `The following parameters do not have values: ${ missingParamNames . join ( ', ' ) }` ; const span = parameterValues . parameterValuesProperty ?. nameValue . span ?? ( parameterValues . deploymentRootObject ? new Span ( parameterValues . deploymentRootObject ?. startIndex , 0 ) : undefined ) ?? new Span ( 0 , 0 ) ; return [ new Issue ( span , message , IssueKind . params_missingRequiredParam ) ] ; } export function findReferencesToDefinitionInParameterValues ( values , definition ) { const results = new ReferenceList ( definition . definitionKind ) ; if ( definition . nameValue ) { const paramValue = values . getParameterValue ( definition . nameValue . unquotedValue ) ; if ( paramValue ) { results . add ( { document : values . document , span : paramValue . nameValue . unquotedSpan } ) ; } } return results ; } /**
 * If this position is inside a parameter value (params file, nested deployment, etc) then
 * return an object with information about this reference and the corresponding definition
 */ export function getReferenceSiteInfoForParameterValue ( definitionsDoc , definitions , scope , values , documentCharacterIndex ) { for ( let paramValue of values . parameterValueDefinitions ) { if ( paramValue . nameValue . span . contains ( documentCharacterIndex , ContainsBehavior . extended ) ) { const paramDef = definitions . parameterDefinitions . find ( d => d . nameValue . unquotedValue . toLowerCase ( ) === paramValue . nameValue . unquotedValue . toLocaleLowerCase ( ) ) ; if ( paramDef ) { return { referenceKind : ReferenceSiteKind . reference , unquotedReferenceSpan : paramValue . nameValue . unquotedSpan , referenceDocument : values . document , definition : paramDef , definitionDocument : definitionsDoc , definitionScope : scope } ; } break ; } } return undefined ; }