export function analyzeCompletion()

in web/ui/module/codemirror-promql/src/complete/hybrid.ts [203:473]


export function analyzeCompletion(state: EditorState, node: SyntaxNode): Context[] {
  const result: Context[] = [];
  switch (node.type.id) {
    case 0: // 0 is the id of the error node
      if (node.parent?.type.id === OffsetExpr) {
        // we are likely in the given situation:
        // `metric_name offset 5` that leads to this tree:
        // `OffsetExpr(VectorSelector(Identifier),Offset,⚠)`
        // Here we can just autocomplete a duration.
        result.push({ kind: ContextKind.Duration });
        break;
      }
      if (node.parent?.type.id === LabelMatcher) {
        // In this case the current token is not itself a valid match op yet:
        //      metric_name{labelName!}
        result.push({ kind: ContextKind.MatchOp });
        break;
      }
      if (node.parent?.type.id === MatrixSelector) {
        // we are likely in the given situation:
        // `metric_name{}[5]`
        // We can also just autocomplete a duration
        result.push({ kind: ContextKind.Duration });
        break;
      }
      if (node.parent?.type.id === SubqueryExpr && containsAtLeastOneChild(node.parent, Duration)) {
        // we are likely in the given situation:
        //    `rate(foo[5d:5])`
        // so we should autocomplete a duration
        result.push({ kind: ContextKind.Duration });
        break;
      }
      // when we are in the situation 'metric_name !', we have the following tree
      // VectorSelector(Identifier,⚠)
      // We should try to know if the char '!' is part of a binOp.
      // Note: as it is quite experimental, maybe it requires more condition and to check the current tree (parent, other child at the same level ..etc.).
      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:
      // sometimes an Identifier has an error has parent. This should be treated in priority
      if (node.parent?.type.id === 0) {
        const errorNodeParent = node.parent.parent;
        if (errorNodeParent?.type.id === StepInvariantExpr) {
          // we are likely in the given situation:
          //   `expr @ s`
          // we can autocomplete start / end
          result.push({ kind: ContextKind.AtModifiers });
          break;
        }
        if (errorNodeParent?.type.id === AggregateExpr) {
          // it matches 'sum() b'. So here we can autocomplete:
          // - the aggregate operation modifier
          // - the binary operation (since it's not mandatory to have an aggregate operation modifier)
          result.push({ kind: ContextKind.AggregateOpModifier }, { kind: ContextKind.BinOp });
          break;
        }
        if (errorNodeParent?.type.id === VectorSelector) {
          // it matches 'sum b'. So here we also have to autocomplete the aggregate operation modifier only
          // if the associated identifier is matching an aggregation operation.
          // Note: here is the corresponding tree in order to understand the situation:
          // VectorSelector(
          //   Identifier,
          //   ⚠(Identifier)
          // )
          const operator = getMetricNameInVectorSelector(node, state);
          if (aggregateOpTerms.filter((term) => term.label === operator).length > 0) {
            result.push({ kind: ContextKind.AggregateOpModifier });
          }
          // It's possible it also match the expr 'metric_name unle'.
          // It's also possible that the operator is also a metric even if it matches the list of aggregation function.
          // So we also have to autocomplete the binary operator.
          //
          // The expr `metric_name off` leads to the same tree. So we have to provide the offset keyword too here.
          result.push({ kind: ContextKind.BinOp }, { kind: ContextKind.Offset });
          break;
        }

        if (errorNodeParent && containsChild(errorNodeParent, 'Expr')) {
          // this last case can appear with the following expression:
          // 1. http_requests_total{method="GET"} off
          // 2. rate(foo[5m]) un
          // 3. sum(http_requests_total{method="GET"} off)
          // For these different cases we have this kind of tree:
          // Parent (
          //    ⚠(Identifier)
          // )
          // We don't really care about the parent, here we are more interested if in the siblings of the error node, there is the node 'Expr'
          // If it is the case, then likely we should autocomplete the BinOp or the offset.
          result.push({ kind: ContextKind.BinOp }, { kind: ContextKind.Offset });
          break;
        }
      }
      // As the leaf Identifier is coming for different cases, we have to take a bit time to analyze the tree
      // in order to know what we have to autocomplete exactly.
      // Here is some cases:
      // 1. metric_name / ignor --> we should autocomplete the BinOpModifier + metric/function/aggregation
      // 2. sum(http_requests_total{method="GET"} / o) --> BinOpModifier + metric/function/aggregation
      // Examples above give a different tree each time and ends up to be treated in this case.
      // But they all have the following common tree pattern:
      // Parent( ...,
      //         ... ,
      //         VectorSelector(Identifier)
      //       )
      //
      // So the first things to do is to get the `Parent` and to determinate if we are in this configuration.
      // Otherwise we would just have to autocomplete the metric / function / aggregation.

      const parent = node.parent?.parent;
      if (!parent) {
        // this case can be possible if the topNode is not anymore PromQL but MetricName.
        // In this particular case, then we just want to autocomplete the metric
        result.push({ kind: ContextKind.MetricName, metricName: state.sliceDoc(node.from, node.to) });
        break;
      }
      // now we have to know if we have two Expr in the direct children of the `parent`
      const containExprTwice = containsChild(parent, 'Expr', 'Expr');
      if (containExprTwice) {
        if (parent.type.id === BinaryExpr && !containsAtLeastOneChild(parent, 0)) {
          // We are likely in the case 1 or 5
          result.push(
            { kind: ContextKind.MetricName, metricName: state.sliceDoc(node.from, node.to) },
            { kind: ContextKind.Function },
            { kind: ContextKind.Aggregation },
            { kind: ContextKind.BinOpModifier },
            { kind: ContextKind.Number }
          );
          // in  case the BinaryExpr is a comparison, we should autocomplete the `bool` keyword. But only if it is not present.
          // When the `bool` keyword is NOT present, then the expression looks like this:
          // 			BinaryExpr( ..., Gtr , ... )
          // When the `bool` keyword is present, then the expression looks like this:
          //      BinaryExpr( ..., Gtr , BoolModifier(...), ... )
          if (containsAtLeastOneChild(parent, Eql, Gte, Gtr, Lte, Lss, Neq) && !containsAtLeastOneChild(parent, BoolModifier)) {
            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 !== FunctionCallBody && parent.type.id !== MatrixSelector) {
          // it's too avoid to autocomplete a number in situation where it shouldn't.
          // Like with `sum by(rat)`
          result.push({ kind: ContextKind.Number });
        }
      }
      break;
    case PromQL:
      if (node.firstChild !== null && node.firstChild.type.id === 0) {
        // this situation can happen when there is nothing in the text area and the user is explicitly triggering the autocompletion (with ctrl + space)
        result.push(
          { kind: ContextKind.MetricName, metricName: '' },
          { kind: ContextKind.Function },
          { kind: ContextKind.Aggregation },
          { kind: ContextKind.Number }
        );
      }
      break;
    case GroupingLabels:
      // In this case we are in the given situation:
      //      sum by () or sum (metric_name) by ()
      // so we have or to autocomplete any kind of labelName or to autocomplete only the labelName associated to the metric
      result.push({ kind: ContextKind.LabelName, metricName: getMetricNameInGroupBy(node, state) });
      break;
    case LabelMatchers:
      // In that case we are in the given situation:
      //       metric_name{} or {}
      // so we have or to autocomplete any kind of labelName or to autocomplete only the labelName associated to the metric
      result.push({ kind: ContextKind.LabelName, metricName: getMetricNameInVectorSelector(node, state) });
      break;
    case LabelName:
      if (node.parent?.type.id === GroupingLabels) {
        // In this case we are in the given situation:
        //      sum by (myL)
        // So we have to continue to autocomplete any kind of labelName
        result.push({ kind: ContextKind.LabelName });
      } else if (node.parent?.type.id === LabelMatcher) {
        // In that case we are in the given situation:
        //       metric_name{myL} or {myL}
        // so we have or to continue to autocomplete any kind of labelName or
        // to continue to autocomplete only the labelName associated to the metric
        result.push({ kind: ContextKind.LabelName, metricName: getMetricNameInVectorSelector(node, state) });
      }
      break;
    case StringLiteral:
      if (node.parent?.type.id === LabelMatcher) {
        // In this case we are in the given situation:
        //      metric_name{labelName=""}
        // So we can autocomplete the labelValue

        // Get the labelName.
        // By definition it's the firstChild: https://github.com/promlabs/lezer-promql/blob/0ef65e196a8db6a989ff3877d57fd0447d70e971/src/promql.grammar#L250
        let labelName = '';
        if (node.parent.firstChild?.type.id === LabelName) {
          labelName = state.sliceDoc(node.parent.firstChild.from, node.parent.firstChild.to);
        }
        // then find the metricName if it exists
        const metricName = getMetricNameInVectorSelector(node, state);
        // finally get the full matcher available
        const matcherNode = walkBackward(node, LabelMatchers);
        const labelMatchers = buildLabelMatchers(matcherNode ? matcherNode.getChildren(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) {
        // Here we are likely in this situation:
        //     `go[5d:4]`
        // and we have the given tree:
        // SubqueryExpr(
        //   VectorSelector(Identifier),
        //   Duration, Duration, ⚠(NumberLiteral)
        // )
        // So we should continue to autocomplete a duration
        result.push({ kind: ContextKind.Duration });
      } else {
        result.push({ kind: ContextKind.Number });
      }
      break;
    case Duration:
    case OffsetExpr:
      result.push({ kind: ContextKind.Duration });
      break;
    case FunctionCallBody:
      // In this case we are in the given situation:
      //       sum() or in rate()
      // with the cursor between the bracket. So we can autocomplete the metric, the function and the aggregation.
      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;
}