import * as fs from 'graceful-fs' ; import * as protobuf from 'protobufjs' ; import * as vscode from 'vscode' ; import { ConfigurationContext } from '../common' ; import { Container } from '../container' ; import { FlagCollection } from '../proto/bazel_flags/FlagCollection' ; import { FlagInfo } from '../proto/bazel_flags/FlagInfo' ; import { isBazelCommand } from './configuration' ; const debug = false ; /**
 * Provide hover & completion for bazel flags.
 */ export class BazelFlagSupport implements vscode . HoverProvider , vscode . CompletionItemProvider < vscode . CompletionItem > , vscode . Disposable { private disposables : vscode . Disposable [ ] = [ ] ; private flagCollection : FlagCollection | undefined ; private flags : Map < string , FlagInfo > | undefined ; constructor ( private configCtx , onDidConfigurationChange ) { onDidConfigurationChange ( this . handleConfiguration , this , this . disposables ) ; this . disposables . push ( vscode . languages . registerHoverProvider ( [ { language : 'bazelrc' , scheme : 'file' } ] , this ) ) ; this . disposables . push ( vscode . languages . registerCompletionItemProvider ( [ { language : 'bazelrc' , scheme : 'file' } ] , this , '-' ) ) ; } private async handleConfiguration ( ) { const collection = await parseFlagCollection ( this . configCtx . protoFile ( 'bazel_flags.proto' ) . fsPath , this . configCtx . extensionFile ( 'flaginfo' , 'bazel.flaginfo' ) . fsPath ) ; this . flagCollection = collection ; this . flags = makeFlagInfoMap ( collection ) ; if ( debug ) { console . log ( `${ collection . flagInfos ?. length } flags, ${ this . flags . size } keys` ) ; console . log ( `${ collection . flagInfos ?. filter ( f => f . hasNegativeFlag ) . length } negatable options` ) ; console . log ( `${ collection . flagInfos ?. filter ( f => f . abbreviation ) . length } short options` ) ; console . log ( `${ collection . flagInfos ?. filter ( f => ! f . abbreviation ) . length } long-only options` ) ; this . flags ?. forEach ( ( flag , name ) => { console . log ( `${ name }\t--${ flag . name }\t-${ flag . abbreviation }` ) ; } ) ; } } public async provideCompletionItems ( document , position , _token , context ) { if ( ! this . flags ) { return ; } const line = document . lineAt ( position . line ) ; if ( ! line || line . isEmptyOrWhitespace ) { return ; } const text = line . text ; if ( /^\s*#/ . test ( text ) ) { return ; } const re = /\s+(--?)([a-z][:_a-zA-Z0-9]*)?/g ; let flagType = '' ; let token = '' ; let match ; while ( ( match = re . exec ( text ) ) !== null ) { let flagLength = match [ 1 ] . length ; if ( match [ 2 ] ) { flagLength += match [ 2 ] . length ; } const start = match . index + match [ 0 ] . length - flagLength ; const end = start + flagLength ; if ( position . character >= start && position . character <= end ) { flagType = match [ 1 ] ; if ( match [ 2 ] ) { token = match [ 2 ] ; } break ; } } if ( ! flagType ) { return ; } const commandName = getCommandNameFromLine ( text ) ; const items = [ ] ; if ( flagType === '-' ) { this . flagCollection ?. flagInfos ?. forEach ( flag => { if ( ! flag . abbreviation ) { return ; } if ( ! flag . abbreviation . startsWith ( token ) ) { return ; } if ( commandName && ! flag . commands ?. includes ( commandName ) ) { return ; } items . push ( makeFlagInfoCompletionItem ( flag . abbreviation , flag ) ) ; } ) ; return items . length ? items : undefined ; } this . flagCollection ?. flagInfos ?. forEach ( flag => { if ( ! flag . name ?. startsWith ( token ) ) { return ; } if ( commandName && ! flag . commands ?. includes ( commandName ) ) { return ; } items . push ( makeFlagInfoCompletionItem ( flag . name , flag ) ) ; } ) ; if ( ! token || token . startsWith ( 'no' ) ) { this . flagCollection ?. flagInfos ?. forEach ( flag => { if ( ! flag . hasNegativeFlag ) { return ; } if ( commandName && ! flag . commands ?. includes ( commandName ) ) { return ; } items . push ( makeFlagInfoCompletionItem ( 'no' + flag . name , flag ) ) ; } ) ; } return items . length ? items : undefined ; } public async provideHover ( document , position , _token ) { if ( ! this . flags ) { return ; } const line = document . lineAt ( position . line ) ; if ( ! line || line . isEmptyOrWhitespace ) { return ; } const re = /\s+(--?)([a-z][:_a-zA-Z0-9]*)/g ; let token ; let range ; let match ; while ( ( match = re . exec ( line . text ) ) !== null ) { const flagLength = match [ 1 ] . length + match [ 2 ] . length ; const start = match . index + match [ 0 ] . length - flagLength ; const end = start + flagLength ; if ( position . character >= start && position . character <= end ) { token = match [ 2 ] ; range = new vscode . Range ( new vscode . Position ( line . lineNumber , start ) , new vscode . Position ( line . lineNumber , end ) ) ; break ; } } if ( ! token ) { return ; } const flag = this . flags . get ( token ) ; if ( ! flag ) { return ; } const hover = makeFlagInfoHover ( flag ) ; hover . range = range ; return hover ; } public dispose ( ) { for ( const disposable of this . disposables ) { disposable . dispose ( ) ; } } } function getCommandNameFromLine ( text ) { let match = null ; if ( ( match = text . match ( /^\s*([-a-z]+)\s+/ ) ) ) { if ( isBazelCommand ( match [ 1 ] ) ) { return match [ 1 ] ; } if ( match [ 1 ] === 'codesearch' ) { return 'query' ; } } return undefined ; } function makeFlagInfoCompletionItem ( name , flag ) { const item = new vscode . CompletionItem ( name , vscode . CompletionItemKind . Constant ) ; item . documentation = flag . documentation ; item . commitCharacters = [ ' ' , '=' ] ; return item ; } async function parseFlagCollection ( protofile , infofile ) { const options = { keepCase : false , longs : String , enums : String , defaults : false , oneofs : true , } ; return protobuf . load ( protofile ) . then ( root => { const FlagCollectionType = root . lookupType ( 'bazel_flags.FlagCollection' ) ; if ( ! FlagCollectionType ) { throw new Error ( 'Failed to find FlagCollection FlagCollectionType' ) ; } const data = fs . readFileSync ( infofile ) ; return FlagCollectionType . toObject ( FlagCollectionType . decode ( data ) , options ) ; } ) ; } param flag function makeFlagInfoMap ( collection ) { const map = new Map ( ) ; const flags = collection . flagInfos ; if ( flags && flags . length ) { for ( const flag of flags ) { if ( ! flag . name ) { continue ; } map . set ( flag . name , flag ) ; if ( flag . hasNegativeFlag ) { map . set ( 'no' + flag . name , flag ) ; } if ( flag . abbreviation ) { map . set ( flag . abbreviation , flag ) ; } } } return map ; } param flag function makeFlagInfoHover ( flag ) { return new vscode . Hover ( makeFlagInfoMarkdown ( flag ) ) ; } param flag function makeFlagInfoMarkdown ( flag ) { let lines = [ ] ; let firstLine = '`--' + flag . name + '`' ; if ( flag . abbreviation ) { firstLine += ' (`-' + flag . abbreviation + '`)' ; } lines . push ( firstLine ) ; lines . push ( '' ) ; lines . push ( flag . documentation || '' ) ; lines . push ( '' ) ; if ( flag . commands ) { lines . push ( flag . commands . map ( c => '`' + c + '`' ) . join ( ', ' ) ) ; } lines . push ( '' ) ; lines . push ( `[Command Line Reference](https://docs.bazel.build/versions/master/command-line-reference.html#flag--${ flag . name }) | ` ) ; lines . push ( `[Code Search](https://cs.opensource.google/search?sq=&ss=bazel%2Fbazel&q=${ flag . name }) ` ) ; return new vscode . MarkdownString ( lines . join ( '\n' ) ) ; }