import { CompleteStrategy } from './index' ; import { SyntaxNode } from '@lezer/common' ; import { PrometheusClient } from '../client' ; import { Add , AggregateExpr , And , BinaryExpr , BinModifiers , Bool , Div , Duration , Eql , EqlRegex , EqlSingle , Expr , FunctionCallArgs , FunctionCallBody , GroupingLabel , GroupingLabels , Gte , Gtr , Identifier , LabelMatcher , LabelMatchers , LabelMatchList , LabelName , Lss , Lte , MatchOp , MatrixSelector , MetricIdentifier , Mod , Mul , Neq , NeqRegex , NumberLiteral , OffsetExpr , Or , Pow , PromQL , StepInvariantExpr , StringLiteral , Sub , SubqueryExpr , Unless , VectorSelector , } from '../grammar/parser.terms' ; import { Completion , CompletionContext , CompletionResult } from '@codemirror/autocomplete' ; import { EditorState } from '@codemirror/state' ; import { buildLabelMatchers , containsAtLeastOneChild , containsChild , retrieveAllRecursiveNodes , walkBackward , walkThrough } from '../parser' ; import { aggregateOpModifierTerms , aggregateOpTerms , atModifierTerms , binOpModifierTerms , binOpTerms , durationTerms , functionIdentifierTerms , matchOpTerms , numberTerms , snippets , } from './promql.terms' ; import { Matcher } from '../types' ; import { syntaxTree } from '@codemirror/language' ; const autocompleteNodes = { matchOp : matchOpTerms , binOp : binOpTerms , duration : durationTerms , binOpModifier : binOpModifierTerms , atModifier : atModifierTerms , functionIdentifier : functionIdentifierTerms , aggregateOp : aggregateOpTerms , aggregateOpModifier : aggregateOpModifierTerms , number : numberTerms , } ; export enum ContextKind { MetricName , LabelName , LabelValue , Function , Aggregation , BinOpModifier , BinOp , MatchOp , AggregateOpModifier , Duration , Offset , Bool , AtModifiers , Number } export interface Context { kind : ContextKind ; metricName ? : string ; labelName ? : string ; matchers ? : Matcher [ ] ; } function getMetricNameInVectorSelector ( tree , state ) { let currentNode = walkBackward ( tree , VectorSelector ) ; if ( ! currentNode ) { return '' ; } currentNode = walkThrough ( currentNode , MetricIdentifier , Identifier ) ; if ( ! currentNode ) { return '' ; } return state . sliceDoc ( currentNode . from , currentNode . to ) ; } function arrayToCompletionResult ( data , from , to , includeSnippet = false , span = true ) { const options = data ; if ( includeSnippet ) { options . push ( ... snippets ) ; } return { from : from , to : to , options : options , span : span ? /^[a-zA-Z0-9_:]+$/ : undefined , } as CompletionResult ; } function computeStartCompleteLabelPositionInLabelMatcherOrInGroupingLabel ( node , pos ) { let start = node . from + 1 ; if ( node . firstChild !== null ) { start = pos ; } return start ; } export function computeStartCompletePosition ( node , pos ) { let start = node . from ; if ( node . type . id === LabelMatchers || node . type . id === GroupingLabels ) { start = computeStartCompleteLabelPositionInLabelMatcherOrInGroupingLabel ( node , pos ) ; } else if ( node . type . id === FunctionCallBody || ( node . type . id === StringLiteral && node . parent ?. type . id === LabelMatcher ) ) { start ++ ; } else if ( node . type . id === OffsetExpr || ( node . type . id === NumberLiteral && node . parent ?. type . id === 0 && node . parent . parent ?. type . id === SubqueryExpr ) || ( node . type . id === 0 && ( node . parent ?. type . id === OffsetExpr || node . parent ?. type . id === MatrixSelector || ( node . parent ?. type . id === SubqueryExpr && containsAtLeastOneChild ( node . parent , Duration ) ) ) ) ) { start = pos ; } return start ; } export function analyzeCompletion ( state , node ) { const result = [ ] ; switch ( node . type . id ) { case 0 : if ( node . parent ?. type . id === OffsetExpr ) { result . push ( { kind : ContextKind . Duration } ) ; break ; } if ( node . parent ?. type . id === LabelMatcher ) { result . push ( { kind : ContextKind . MatchOp } ) ; break ; } if ( node . parent ?. type . id === MatrixSelector ) { result . push ( { kind : ContextKind . Duration } ) ; break ; } if ( node . parent ?. type . id === SubqueryExpr && containsAtLeastOneChild ( node . parent , Duration ) ) { result . push ( { kind : ContextKind . Duration } ) ; break ; } const operator = state . sliceDoc ( node . from , node . to ) ; if ( binOpTerms . filter ( ( term ) => term . label . includes ( operator ) ) . length > 0 ) { result . push ( { kind : ContextKind . BinOp } ) ; } break ; case Identifier : if ( node . parent ?. type . id === 0 ) { const errorNodeParent = node . parent . parent ; if ( errorNodeParent ?. type . id === StepInvariantExpr ) { result . push ( { kind : ContextKind . AtModifiers } ) ; break ; } if ( errorNodeParent ?. type . id === AggregateExpr ) { result . push ( { kind : ContextKind . AggregateOpModifier } , { kind : ContextKind . BinOp } ) ; break ; } if ( errorNodeParent ?. type . id === VectorSelector ) { const operator = getMetricNameInVectorSelector ( node , state ) ; if ( aggregateOpTerms . filter ( ( term ) => term . label === operator ) . length > 0 ) { result . push ( { kind : ContextKind . AggregateOpModifier } ) ; } result . push ( { kind : ContextKind . BinOp } , { kind : ContextKind . Offset } ) ; break ; } if ( errorNodeParent && containsChild ( errorNodeParent , Expr ) ) { result . push ( { kind : ContextKind . BinOp } , { kind : ContextKind . Offset } ) ; break ; } } const parent = node . parent ?. parent ?. parent ?. parent ; if ( ! parent ) { result . push ( { kind : ContextKind . MetricName , metricName : state . sliceDoc ( node . from , node . to ) } ) ; break ; } const containExprTwice = containsChild ( parent , Expr , Expr ) ; if ( containExprTwice ) { if ( parent . type . id === BinaryExpr && ! containsAtLeastOneChild ( parent , 0 ) ) { result . push ( { kind : ContextKind . MetricName , metricName : state . sliceDoc ( node . from , node . to ) } , { kind : ContextKind . Function } , { kind : ContextKind . Aggregation } , { kind : ContextKind . BinOpModifier } , { kind : ContextKind . Number } ) ; if ( containsAtLeastOneChild ( parent , Eql , Gte , Gtr , Lte , Lss , Neq ) && ! walkThrough ( parent , BinModifiers , Bool ) ) { result . push ( { kind : ContextKind . Bool } ) ; } } } else { result . push ( { kind : ContextKind . MetricName , metricName : state . sliceDoc ( node . from , node . to ) } , { kind : ContextKind . Function } , { kind : ContextKind . Aggregation } ) ; if ( parent . type . id !== FunctionCallArgs && parent . type . id !== MatrixSelector ) { result . push ( { kind : ContextKind . Number } ) ; } } break ; case PromQL : if ( node . firstChild !== null && node . firstChild . type . id === 0 ) { result . push ( { kind : ContextKind . MetricName , metricName : '' } , { kind : ContextKind . Function } , { kind : ContextKind . Aggregation } , { kind : ContextKind . Number } ) ; } break ; case GroupingLabels : result . push ( { kind : ContextKind . LabelName } ) ; break ; case LabelMatchers : result . push ( { kind : ContextKind . LabelName , metricName : getMetricNameInVectorSelector ( node , state ) } ) ; break ; case LabelName : if ( node . parent ?. type . id === GroupingLabel ) { result . push ( { kind : ContextKind . LabelName } ) ; } else if ( node . parent ?. type . id === LabelMatcher ) { result . push ( { kind : ContextKind . LabelName , metricName : getMetricNameInVectorSelector ( node , state ) } ) ; } break ; case StringLiteral : if ( node . parent ?. type . id === LabelMatcher ) { let labelName = '' ; if ( node . parent . firstChild ?. type . id === LabelName ) { labelName = state . sliceDoc ( node . parent . firstChild . from , node . parent . firstChild . to ) ; } const metricName = getMetricNameInVectorSelector ( node , state ) ; const labelMatchers = buildLabelMatchers ( retrieveAllRecursiveNodes ( walkBackward ( node , LabelMatchList ) , LabelMatchList , LabelMatcher ) , state ) ; result . push ( { kind : ContextKind . LabelValue , metricName : metricName , labelName : labelName , matchers : labelMatchers , } ) ; } break ; case NumberLiteral : if ( node . parent ?. type . id === 0 && node . parent . parent ?. type . id === SubqueryExpr ) { result . push ( { kind : ContextKind . Duration } ) ; } else { result . push ( { kind : ContextKind . Number } ) ; } break ; case Duration : case OffsetExpr : result . push ( { kind : ContextKind . Duration } ) ; break ; case FunctionCallBody : result . push ( { kind : ContextKind . MetricName , metricName : '' } , { kind : ContextKind . Function } , { kind : ContextKind . Aggregation } ) ; break ; case Neq : if ( node . parent ?. type . id === MatchOp ) { result . push ( { kind : ContextKind . MatchOp } ) ; } else if ( node . parent ?. type . id === BinaryExpr ) { result . push ( { kind : ContextKind . BinOp } ) ; } break ; case EqlSingle : case EqlRegex : case NeqRegex : case MatchOp : result . push ( { kind : ContextKind . MatchOp } ) ; break ; case Pow : case Mul : case Div : case Mod : case Add : case Sub : case Eql : case Gte : case Gtr : case Lte : case Lss : case And : case Unless : case Or : case BinaryExpr : result . push ( { kind : ContextKind . BinOp } ) ; break ; } return result ; } export class HybridComplete implements CompleteStrategy { private readonly prometheusClient : PrometheusClient | undefined ; private readonly maxMetricsMetadata : number ; constructor ( prometheusClient ? , maxMetricsMetadata = 10000 ) { this . prometheusClient = prometheusClient ; this . maxMetricsMetadata = maxMetricsMetadata ; } getPrometheusClient ( ) { return this . prometheusClient ; } promQL ( context ) { const { state , pos } = context ; const tree = syntaxTree ( state ) . resolve ( pos , - 1 ) ; const contexts = analyzeCompletion ( state , tree ) ; let asyncResult = Promise . resolve ( [ ] ) ; let completeSnippet = false ; let span = true ; for ( const context of contexts ) { switch ( context . kind ) { case ContextKind . Aggregation : completeSnippet = true ; asyncResult = asyncResult . then ( ( result ) => { return result . concat ( autocompleteNodes . aggregateOp ) ; } ) ; break ; case ContextKind . Function : completeSnippet = true ; asyncResult = asyncResult . then ( ( result ) => { return result . concat ( autocompleteNodes . functionIdentifier ) ; } ) ; break ; case ContextKind . BinOpModifier : asyncResult = asyncResult . then ( ( result ) => { return result . concat ( autocompleteNodes . binOpModifier ) ; } ) ; break ; case ContextKind . BinOp : asyncResult = asyncResult . then ( ( result ) => { return result . concat ( autocompleteNodes . binOp ) ; } ) ; break ; case ContextKind . MatchOp : asyncResult = asyncResult . then ( ( result ) => { return result . concat ( autocompleteNodes . matchOp ) ; } ) ; break ; case ContextKind . AggregateOpModifier : asyncResult = asyncResult . then ( ( result ) => { return result . concat ( autocompleteNodes . aggregateOpModifier ) ; } ) ; break ; case ContextKind . Duration : span = false ; asyncResult = asyncResult . then ( ( result ) => { return result . concat ( autocompleteNodes . duration ) ; } ) ; break ; case ContextKind . Offset : asyncResult = asyncResult . then ( ( result ) => { return result . concat ( [ { label : 'offset' } ] ) ; } ) ; break ; case ContextKind . Bool : asyncResult = asyncResult . then ( ( result ) => { return result . concat ( [ { label : 'bool' } ] ) ; } ) ; break ; case ContextKind . AtModifiers : asyncResult = asyncResult . then ( ( result ) => { return result . concat ( autocompleteNodes . atModifier ) ; } ) ; break ; case ContextKind . Number : asyncResult = asyncResult . then ( ( result ) => { return result . concat ( autocompleteNodes . number ) ; } ) ; break ; case ContextKind . MetricName : asyncResult = asyncResult . then ( ( result ) => { return this . autocompleteMetricName ( result , context ) ; } ) ; break ; case ContextKind . LabelName : asyncResult = asyncResult . then ( ( result ) => { return this . autocompleteLabelName ( result , context ) ; } ) ; break ; case ContextKind . LabelValue : asyncResult = asyncResult . then ( ( result ) => { return this . autocompleteLabelValue ( result , context ) ; } ) ; } } return asyncResult . then ( ( result ) => { return arrayToCompletionResult ( result , computeStartCompletePosition ( tree , pos ) , pos , completeSnippet , span ) ; } ) ; } private autocompleteMetricName ( result , context ) { if ( ! this . prometheusClient ) { return result ; } const metricCompletion = new Map < string , Completion > ( ) ; return this . prometheusClient . metricNames ( context . metricName ) . then ( ( metricNames ) => { for ( const metricName of metricNames ) { metricCompletion . set ( metricName , { label : metricName , type : 'constant' } ) ; } if ( metricNames . length <= this . maxMetricsMetadata ) { return this . prometheusClient ?. metricMetadata ( ) ; } } ) . then ( ( metricMetadata ) => { if ( metricMetadata ) { for ( const [ metricName , node ] of metricCompletion ) { const metadata = metricMetadata [ metricName . replace ( /(_count|_sum|_bucket)$/ , '' ) ] ; if ( metadata ) { if ( metadata . length > 1 ) { for ( const m of metadata ) { if ( node . detail === '' ) { node . detail = m . type ; } else if ( node . detail !== m . type ) { node . detail = 'unknown' ; node . info = 'multiple different definitions for this metric' ; } if ( node . info === '' ) { node . info = m . help ; } else if ( node . info !== m . help ) { node . info = 'multiple different definitions for this metric' ; } } } else if ( metadata . length === 1 ) { let { type , help } = metadata [ 0 ] ; if ( type === 'histogram' || type === 'summary' ) { if ( metricName . endsWith ( '_count' ) ) { type = 'counter' ; help = `The total number of observations for: ${ help }` ; } if ( metricName . endsWith ( '_sum' ) ) { type = 'counter' ; help = `The total sum of observations for: ${ help }` ; } if ( metricName . endsWith ( '_bucket' ) ) { type = 'counter' ; help = `The total count of observations for a bucket in the histogram: ${ help }` ; } } node . detail = type ; node . info = help ; } } } } return result . concat ( Array . from ( metricCompletion . values ( ) ) ) ; } ) ; } private autocompleteLabelName ( result , context ) { if ( ! this . prometheusClient ) { return result ; } return this . prometheusClient . labelNames ( context . metricName ) . then ( ( labelNames ) => { return result . concat ( labelNames . map ( ( value ) => ( { label : value , type : 'constant' } ) ) ) ; } ) ; } private autocompleteLabelValue ( result , context ) { if ( ! this . prometheusClient || ! context . labelName ) { return result ; } return this . prometheusClient . labelValues ( context . labelName , context . metricName , context . matchers ) . then ( ( labelValues ) => { return result . concat ( labelValues . map ( ( value ) => ( { label : value , type : 'text' } ) ) ) ; } ) ; } }