import { CompletionItem , CompletionItemKind , MarkupContent , Range , TextEdit , } from 'vscode-languageserver' ; import { Position , TextDocument } from 'vscode-languageserver-textdocument' ; import { Node , Scalar , YAMLMap } from 'yaml/types' ; import { IOption } from '../interfaces/module' ; import { WorkspaceFolderContext } from '../services/workspaceManager' ; import { blockKeywords , playKeywords , playWithoutTaskKeywords , roleKeywords , taskKeywords , } from '../utils/ansible' ; import { formatModule , formatOption , getDetails } from '../utils/docsFormatter' ; import { insert , toLspRange } from '../utils/misc' ; import { AncestryBuilder , findProvidedModule , getDeclaredCollections , getPathAt , getOrigRange , getYamlMapKeys , isBlockParam , isPlayParam , isRoleParam , isTaskParam , parseAllDocuments , getPossibleOptionsForPath , } from '../utils/yaml' ; const priorityMap = { nameKeyword : 1 , moduleName : 2 , redirectedModuleName : 3 , keyword : 4 , requiredOption : 1 , option : 2 , aliasOption : 3 , } ; export async function doCompletion ( document , position , context ) { let preparedText = document . getText ( ) ; const offset = document . offsetAt ( position ) ; preparedText = insert ( preparedText , offset , '_:' ) ; const yamlDocs = parseAllDocuments ( preparedText ) ; const useFqcn = ( await context . documentSettings . get ( document . uri ) ) . ansible . useFullyQualifiedCollectionNames ; const path = getPathAt ( document , position , yamlDocs , true ) ; if ( path ) { const node = path [ path . length - 1 ] ; if ( node ) { const docsLibrary = await context . docsLibrary ; const isPlay = isPlayParam ( path ) ; if ( isPlay ) { return getKeywordCompletion ( document , position , path , playKeywords ) ; } if ( isBlockParam ( path ) ) { return getKeywordCompletion ( document , position , path , blockKeywords ) ; } if ( isRoleParam ( path ) ) { return getKeywordCompletion ( document , position , path , roleKeywords ) ; } if ( isTaskParam ( path ) ) { const completionItems = getKeywordCompletion ( document , position , path , taskKeywords ) ; if ( isPlay === undefined ) { completionItems . push ( ... getKeywordCompletion ( document , position , path , playWithoutTaskKeywords ) ) ; } const module = await findProvidedModule ( path , document , docsLibrary ) ; if ( ! module ) { completionItems . push ( ... getKeywordCompletion ( document , position , path , new Map ( [ [ 'block' , blockKeywords . get ( 'block' ) as string ] ] ) ) ) ; const inlineCollections = getDeclaredCollections ( path ) ; const cursorAtEndOfLine = atEndOfLine ( document , position ) ; let textEdit ; const nodeRange = getNodeRange ( node , document ) ; if ( nodeRange ) { textEdit = { range : nodeRange , newText : '' , } ; } const moduleCompletionItems = [ ... docsLibrary . moduleFqcns ] . map ( ( moduleFqcn ) : CompletionItem => { let priority , kind ; if ( docsLibrary . getModuleRoute ( moduleFqcn ) ?. redirect ) { priority = priorityMap . redirectedModuleName ; kind = CompletionItemKind . Reference ; } else { priority = priorityMap . moduleName ; kind = CompletionItemKind . Class ; } const [ namespace , collection , name ] = moduleFqcn . split ( '.' ) ; return { label : useFqcn ? moduleFqcn : name , kind : kind , detail : `${ namespace }.${ collection }` , sortText : useFqcn ? `${ priority }_${ moduleFqcn }` : `${ priority }_${ name }` , filterText : useFqcn ? `${ name } ${ moduleFqcn } ${ collection } ${ namespace }` : `${ name } ${ moduleFqcn }` , data : { documentUri : document . uri , moduleFqcn : moduleFqcn , inlineCollections : inlineCollections , atEndOfLine : cursorAtEndOfLine , } , textEdit : textEdit , } ; } ) ; completionItems . push ( ... moduleCompletionItems ) ; } return completionItems ; } const options = await getPossibleOptionsForPath ( path , document , docsLibrary ) ; if ( options ) { const optionMap = new AncestryBuilder ( path ) . parentOfKey ( ) . get ( ) as YAMLMap ; const providedOptions = new Set ( getYamlMapKeys ( optionMap ) ) ; const remainingOptions = [ ... options . entries ( ) ] . filter ( ( [ , specs ] ) => ! providedOptions . has ( specs . name ) ) ; const nodeRange = getNodeRange ( node , document ) ; return remainingOptions . map ( ( [ option , specs ] ) => { return { name : option , specs : specs , } ; } ) . map ( ( option , index ) => { const details = getDetails ( option . specs ) ; let priority ; if ( isAlias ( option ) ) { priority = priorityMap . aliasOption ; } else if ( option . specs . required ) { priority = priorityMap . requiredOption ; } else { priority = priorityMap . option ; } const completionItem = { label : option . name , detail : details , sortText : priority . toString ( ) + index . toString ( ) . padStart ( 3 ) , kind : isAlias ( option ) ? CompletionItemKind . Reference : CompletionItemKind . Property , documentation : formatOption ( option . specs ) , insertText : atEndOfLine ( document , position ) ? `${ option . name }:` : undefined , } ; const insertText = atEndOfLine ( document , position ) ? `${ option . name }:` : option . name ; if ( nodeRange ) { completionItem . textEdit = { range : nodeRange , newText : insertText , } ; } else { completionItem . insertText = insertText ; } return completionItem ; } ) ; } } } return null ; } function getKeywordCompletion ( document , position , path , keywords ) { const parameterMap = new AncestryBuilder ( path ) . parent ( YAMLMap ) . get ( ) as YAMLMap ; const providedParams = new Set ( getYamlMapKeys ( parameterMap ) ) ; const remainingParams = [ ... keywords . entries ( ) ] . filter ( ( [ keyword ] ) => ! providedParams . has ( keyword ) ) ; const nodeRange = getNodeRange ( path [ path . length - 1 ] , document ) ; return remainingParams . map ( ( [ keyword , description ] ) => { const priority = keyword === 'name' ? priorityMap . nameKeyword : priorityMap . keyword ; const completionItem = { label : keyword , kind : CompletionItemKind . Property , sortText : `${ priority }_${ keyword }` , documentation : description , } ; const insertText = atEndOfLine ( document , position ) ? `${ keyword }:` : keyword ; if ( nodeRange ) { completionItem . textEdit = { range : nodeRange , newText : insertText , } ; } else { completionItem . insertText = insertText ; } return completionItem ; } ) ; } /**
 * Returns an LSP formatted range compensating for the `_:` hack, provided that
 * the node has range information and is a string scalar.
 */ function getNodeRange ( node , document ) { const range = getOrigRange ( node ) ; if ( range && node instanceof Scalar && typeof node . value === 'string' ) { const start = range [ 0 ] ; let end = range [ 1 ] ; if ( node . value . includes ( '_:' ) ) { end -= 2 ; } else { end -= 1 ; } return toLspRange ( [ start , end ] , document ) ; } } export async function doCompletionResolve ( completionItem , context ) { if ( completionItem . data ?. moduleFqcn && completionItem . data ?. documentUri ) { const docsLibrary = await context . docsLibrary ; const [ module ] = await docsLibrary . findModule ( completionItem . data . moduleFqcn ) ; if ( module && module . documentation ) { const [ namespace , collection , name ] = completionItem . data . moduleFqcn . split ( '.' ) ; let useFqcn = ( await context . documentSettings . get ( completionItem . data . documentUri ) ) . ansible . useFullyQualifiedCollectionNames ; if ( ! useFqcn ) { const declaredCollections = completionItem . data ?. inlineCollections || [ ] ; declaredCollections . push ( 'ansible.builtin' ) ; const metadata = await context . documentMetadata . get ( completionItem . data . documentUri ) ; if ( metadata ) { declaredCollections . push ( ... metadata . collections ) ; } const canUseShortName = declaredCollections . some ( ( c ) => c === `${ namespace }.${ collection }` ) ; if ( ! canUseShortName ) { useFqcn = true ; } } const insertName = useFqcn ? completionItem . data . moduleFqcn : name ; const insertText = completionItem . data . atEndOfLine ? `${ insertName }:` : insertName ; if ( completionItem . textEdit ) { completionItem . textEdit . newText = insertText ; } else { completionItem . insertText = insertText ; } completionItem . documentation = formatModule ( module . documentation , docsLibrary . getModuleRoute ( completionItem . data . moduleFqcn ) ) ; } } return completionItem ; } function isAlias ( option ) { return option . name !== option . specs . name ; } function atEndOfLine ( document , position ) { const charAfterCursor = `${ document . getText ( ) }\n` [ document . offsetAt ( position ) ] ; return charAfterCursor === '\n' || charAfterCursor === '\r' ; }