import * as vscode from 'vscode' ; import * as yp from 'yaml-ast-parser' ; import * as _ from 'lodash' ; export class HelmDocumentSymbolProvider implements vscode . DocumentSymbolProvider { provideDocumentSymbols ( document , token ) { return this . provideDocumentSymbolsImpl ( document , token ) ; } async provideDocumentSymbolsImpl ( document , _token ) { const fakeText1 = document . getText ( ) ; const fakeText2 = substituteStatements ( fakeText1 ) ; const fakeText3 = substituteExpressions ( fakeText2 ) ; const root = yp . safeLoad ( fakeText3 ) ; const syms = [ ] ; walk ( root , '' , document , document . uri , syms ) ; return syms ; } } function substituteStatements ( text ) { return text . split ( '\n' ) . map ( ( l ) => substituteStatement ( l ) ) . join ( '\n' ) ; } function substituteStatement ( line ) { if ( line . trim ( ) . startsWith ( '{{' ) ) { return line . replace ( '{{' , '#{' ) ; } return line ; } function substituteExpressions ( text ) { return text . replace ( /{{[^}]*}}/g , ( s ) => encodeWithTemplateMarkers ( s ) ) ; } const ENCODE_TEMPLATE_START = 'AA' ; const ENCODE_TEMPLATE_END = 'ZZ' ; const ENCODE_TEMPLATE_QUOTE = 'Q' ; function encodeWithTemplateMarkers ( s ) { return s . replace ( /{{/g , ENCODE_TEMPLATE_START ) . replace ( /}}/g , ENCODE_TEMPLATE_END ) . replace ( /"/g , ENCODE_TEMPLATE_QUOTE ) ; } export interface FoundKeyPath { readonly found : vscode . SymbolInformation | undefined ; readonly remaining : string [ ] ; } export function findKeyPath ( keyPath , sis ) { return findKeyPathAcc ( keyPath , sis , undefined ) ; } function findKeyPathAcc ( keyPath , sis , acc ) { const parentSym = findKey ( keyPath [ 0 ] , sis ) ; if ( ! parentSym ) { return { found : acc , remaining : keyPath } ; } if ( keyPath . length === 1 ) { return { found : parentSym , remaining : [ ] } ; } const childSyms = sis . filter ( ( s ) => parentSym . location . range . contains ( s . location . range ) ) ; return findKeyPathAcc ( keyPath . slice ( 1 ) , childSyms , parentSym ) ; } function findKey ( key , sis ) { const fields = sis . filter ( ( si ) => si . kind === vscode . SymbolKind . Field && si . name === key ) ; if ( fields . length === 0 ) { return undefined ; } return outermost ( fields ) ; } function outermost ( sis ) { return _ . maxBy ( sis , ( s ) => containmentChain ( s , sis ) ) ! ; } export function containmentChain ( s , sis ) { const containers = sis . filter ( ( si ) => si . kind === vscode . SymbolKind . Field ) . filter ( ( si ) => si . location . range . contains ( s . location . range ) ) . filter ( ( si ) => si !== s ) ; if ( containers . length === 0 ) { return [ ] ; } const nextUp = minimalSymbol ( containers ) ; const fromThere = containmentChain ( nextUp , sis ) ; return [ nextUp , ... fromThere ] ; } export function symbolAt ( position , sis ) { const containers = sis . filter ( ( si ) => si . location . range . contains ( position ) ) ; if ( containers . length === 0 ) { return undefined ; } return minimalSymbol ( containers ) ; } function minimalSymbol ( sis ) { let m = sis [ 0 ] ; for ( const si of sis ) { if ( m . location . range . contains ( si . location . range ) ) { m = si ; } } return m ; } function symbolInfo ( node , containerName , d , uri ) { const start = node . startPosition ; const end = node . endPosition ; const loc = new vscode . Location ( uri , new vscode . Range ( d . positionAt ( start ) , d . positionAt ( end ) ) ) ; switch ( node . kind ) { case yp . Kind . ANCHOR_REF : return new vscode . SymbolInformation ( `ANCHOR_REF` , vscode . SymbolKind . Variable , containerName , loc ) ; case yp . Kind . INCLUDE_REF : return new vscode . SymbolInformation ( `INCLUDE_REF` , vscode . SymbolKind . Variable , containerName , loc ) ; case yp . Kind . MAP : return new vscode . SymbolInformation ( `{map}` , vscode . SymbolKind . Variable , containerName , loc ) ; case yp . Kind . MAPPING : const mp = node as yp . YAMLMapping ; return new vscode . SymbolInformation ( `${ mp . key . rawValue }` , vscode . SymbolKind . Field , containerName , loc ) ; case yp . Kind . SCALAR : const realValue = d . getText ( loc . range ) || '(scalar)' ; const isTemplateExpr = ( realValue . startsWith ( '{{' ) && realValue . endsWith ( '}}' ) ) || ( realValue . startsWith ( '"{{' ) && realValue . endsWith ( '}}"' ) ) ; const symbolKind = isTemplateExpr ? vscode . SymbolKind . Object : vscode . SymbolKind . Constant ; return new vscode . SymbolInformation ( realValue , symbolKind , containerName , loc ) ; case yp . Kind . SEQ : return new vscode . SymbolInformation ( `[seq]` , vscode . SymbolKind . Variable , containerName , loc ) ; } return new vscode . SymbolInformation ( `###_YAML_UNEXPECTED_###` , vscode . SymbolKind . Variable , containerName , loc ) ; } function walk ( node , containerName , d , uri , syms ) { const sym = symbolInfo ( node , containerName , d , uri ) ; syms . push ( sym ) ; switch ( node . kind ) { case yp . Kind . ANCHOR_REF : return ; case yp . Kind . INCLUDE_REF : return ; case yp . Kind . MAP : const m = node as yp . YamlMap ; for ( const mm of m . mappings ) { walk ( mm , sym . name , d , uri , syms ) ; } return ; case yp . Kind . MAPPING : const mp = node as yp . YAMLMapping ; if ( mp . value ) { walk ( mp . value , sym . name , d , uri , syms ) ; } return ; case yp . Kind . SCALAR : return ; case yp . Kind . SEQ : const s = node as yp . YAMLSequence ; for ( const y of s . items ) { walk ( y , sym . name , d , uri , syms ) ; } return ; } }