Merge pull request #14931 from prometheus/nexucis/autocomplete-topl
Some checks are pending
buf.build / lint and publish (push) Waiting to run
CI / Go tests (push) Waiting to run
CI / More Go tests (push) Waiting to run
CI / Go tests with previous Go version (push) Waiting to run
CI / UI tests (push) Waiting to run
CI / Go tests on Windows (push) Waiting to run
CI / Mixins tests (push) Waiting to run
CI / Build Prometheus for common architectures (0) (push) Waiting to run
CI / Build Prometheus for common architectures (1) (push) Waiting to run
CI / Build Prometheus for common architectures (2) (push) Waiting to run
CI / Build Prometheus for all architectures (0) (push) Waiting to run
CI / Build Prometheus for all architectures (1) (push) Waiting to run
CI / Build Prometheus for all architectures (10) (push) Waiting to run
CI / Build Prometheus for all architectures (11) (push) Waiting to run
CI / Build Prometheus for all architectures (2) (push) Waiting to run
CI / Build Prometheus for all architectures (3) (push) Waiting to run
CI / Build Prometheus for all architectures (4) (push) Waiting to run
CI / Build Prometheus for all architectures (5) (push) Waiting to run
CI / Build Prometheus for all architectures (6) (push) Waiting to run
CI / Build Prometheus for all architectures (7) (push) Waiting to run
CI / Build Prometheus for all architectures (8) (push) Waiting to run
CI / Build Prometheus for all architectures (9) (push) Waiting to run
CI / Report status of build Prometheus for all architectures (push) Blocked by required conditions
CI / Check generated parser (push) Waiting to run
CI / golangci-lint (push) Waiting to run
CI / fuzzing (push) Waiting to run
CI / codeql (push) Waiting to run
CI / Publish main branch artifacts (push) Blocked by required conditions
CI / Publish release artefacts (push) Blocked by required conditions
CI / Publish UI on npm Registry (push) Blocked by required conditions
Scorecards supply-chain security / Scorecards analysis (push) Waiting to run

UI/PromQL: autocomplete topk like aggregation function parameters
This commit is contained in:
Julius Volz 2024-09-19 18:12:59 +02:00 committed by GitHub
commit e480cf21eb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 74 additions and 6 deletions

View file

@ -583,12 +583,42 @@ describe('analyzeCompletion test', () => {
pos: 5, pos: 5,
expectedContext: [{ kind: ContextKind.AtModifiers }], expectedContext: [{ kind: ContextKind.AtModifiers }],
}, },
{
title: 'autocomplete topk params',
expr: 'topk()',
pos: 5,
expectedContext: [{ kind: ContextKind.Number }],
},
{
title: 'autocomplete topk params 2',
expr: 'topk(inf,)',
pos: 9,
expectedContext: [{ kind: ContextKind.MetricName, metricName: '' }, { kind: ContextKind.Function }, { kind: ContextKind.Aggregation }],
},
{
title: 'autocomplete topk params 3',
expr: 'topk(inf,r)',
pos: 10,
expectedContext: [{ kind: ContextKind.MetricName, metricName: 'r' }, { kind: ContextKind.Function }, { kind: ContextKind.Aggregation }],
},
{
title: 'autocomplete topk params 4',
expr: 'topk by(instance) ()',
pos: 19,
expectedContext: [{ kind: ContextKind.Number }],
},
{
title: 'autocomplete topk params 5',
expr: 'topk by(instance) (inf,r)',
pos: 24,
expectedContext: [{ kind: ContextKind.MetricName, metricName: 'r' }, { kind: ContextKind.Function }, { kind: ContextKind.Aggregation }],
},
]; ];
testCases.forEach((value) => { testCases.forEach((value) => {
it(value.title, () => { it(value.title, () => {
const state = createEditorState(value.expr); const state = createEditorState(value.expr);
const node = syntaxTree(state).resolve(value.pos, -1); const node = syntaxTree(state).resolve(value.pos, -1);
const result = analyzeCompletion(state, node); const result = analyzeCompletion(state, node, value.pos);
expect(value.expectedContext).toEqual(result); expect(value.expectedContext).toEqual(result);
}); });
}); });

View file

@ -54,6 +54,12 @@ import {
QuotedLabelName, QuotedLabelName,
NumberDurationLiteralInDurationContext, NumberDurationLiteralInDurationContext,
NumberDurationLiteral, NumberDurationLiteral,
AggregateOp,
Topk,
Bottomk,
LimitK,
LimitRatio,
CountValues,
} from '@prometheus-io/lezer-promql'; } from '@prometheus-io/lezer-promql';
import { Completion, CompletionContext, CompletionResult } from '@codemirror/autocomplete'; import { Completion, CompletionContext, CompletionResult } from '@codemirror/autocomplete';
import { EditorState } from '@codemirror/state'; import { EditorState } from '@codemirror/state';
@ -185,7 +191,7 @@ export function computeStartCompletePosition(state: EditorState, node: SyntaxNod
if (node.type.id === LabelMatchers || node.type.id === GroupingLabels) { if (node.type.id === LabelMatchers || node.type.id === GroupingLabels) {
start = computeStartCompleteLabelPositionInLabelMatcherOrInGroupingLabel(node, pos); start = computeStartCompleteLabelPositionInLabelMatcherOrInGroupingLabel(node, pos);
} else if ( } else if (
node.type.id === FunctionCallBody || (node.type.id === FunctionCallBody && node.firstChild === null) ||
(node.type.id === StringLiteral && (node.parent?.type.id === UnquotedLabelMatcher || node.parent?.type.id === QuotedLabelMatcher)) (node.type.id === StringLiteral && (node.parent?.type.id === UnquotedLabelMatcher || node.parent?.type.id === QuotedLabelMatcher))
) { ) {
// When the cursor is between bracket, quote, we need to increment the starting position to avoid to consider the open bracket/ first string. // When the cursor is between bracket, quote, we need to increment the starting position to avoid to consider the open bracket/ first string.
@ -198,6 +204,7 @@ export function computeStartCompletePosition(state: EditorState, node: SyntaxNod
// So we have to analyze the string about the current node to see if the duration unit is already present or not. // So we have to analyze the string about the current node to see if the duration unit is already present or not.
(node.type.id === NumberDurationLiteralInDurationContext && !durationTerms.map((v) => v.label).includes(currentText[currentText.length - 1])) || (node.type.id === NumberDurationLiteralInDurationContext && !durationTerms.map((v) => v.label).includes(currentText[currentText.length - 1])) ||
(node.type.id === NumberDurationLiteral && node.parent?.type.id === 0 && node.parent.parent?.type.id === SubqueryExpr) || (node.type.id === NumberDurationLiteral && node.parent?.type.id === 0 && node.parent.parent?.type.id === SubqueryExpr) ||
(node.type.id === FunctionCallBody && isAggregatorWithParam(node) && node.firstChild !== null) ||
(node.type.id === 0 && (node.type.id === 0 &&
(node.parent?.type.id === OffsetExpr || (node.parent?.type.id === OffsetExpr ||
node.parent?.type.id === MatrixSelector || node.parent?.type.id === MatrixSelector ||
@ -208,10 +215,21 @@ export function computeStartCompletePosition(state: EditorState, node: SyntaxNod
return start; return start;
} }
function isAggregatorWithParam(functionCallBody: SyntaxNode): boolean {
const parent = functionCallBody.parent;
if (parent !== null && parent.firstChild?.type.id === AggregateOp) {
const aggregationOpType = parent.firstChild.firstChild;
if (aggregationOpType !== null && [Topk, Bottomk, LimitK, LimitRatio, CountValues].includes(aggregationOpType.type.id)) {
return true;
}
}
return false;
}
// analyzeCompletion is going to determinate what should be autocompleted. // analyzeCompletion is going to determinate what should be autocompleted.
// The value of the autocompletion is then calculate by the function buildCompletion. // The value of the autocompletion is then calculate by the function buildCompletion.
// Note: this method is exported for testing purpose only. Do not use it directly. // Note: this method is exported for testing purpose only. Do not use it directly.
export function analyzeCompletion(state: EditorState, node: SyntaxNode): Context[] { export function analyzeCompletion(state: EditorState, node: SyntaxNode, pos: number): Context[] {
const result: Context[] = []; const result: Context[] = [];
switch (node.type.id) { switch (node.type.id) {
case 0: // 0 is the id of the error node case 0: // 0 is the id of the error node
@ -330,7 +348,7 @@ export function analyzeCompletion(state: EditorState, node: SyntaxNode): Context
} }
// now we have to know if we have two Expr in the direct children of the `parent` // now we have to know if we have two Expr in the direct children of the `parent`
const containExprTwice = containsChild(parent, 'Expr', 'Expr'); const containExprTwice = containsChild(parent, 'Expr', 'Expr');
if (containExprTwice) { if (containExprTwice && parent.type.id !== FunctionCallBody) {
if (parent.type.id === BinaryExpr && !containsAtLeastOneChild(parent, 0)) { if (parent.type.id === BinaryExpr && !containsAtLeastOneChild(parent, 0)) {
// We are likely in the case 1 or 5 // We are likely in the case 1 or 5
result.push( result.push(
@ -460,7 +478,23 @@ export function analyzeCompletion(state: EditorState, node: SyntaxNode): Context
result.push({ kind: ContextKind.Duration }); result.push({ kind: ContextKind.Duration });
break; break;
case FunctionCallBody: case FunctionCallBody:
// In this case we are in the given situation: // For aggregation function such as Topk, the first parameter is a number.
// The second one is an expression.
// When moving to the second parameter, the node is an error node.
// Unfortunately, as a current node, codemirror doesn't give us the error node but instead the FunctionCallBody
// The tree looks like that: PromQL(AggregateExpr(AggregateOp(Topk),FunctionCallBody(NumberDurationLiteral,⚠)))
// So, we need to figure out if the cursor is on the first parameter or in the second.
if (isAggregatorWithParam(node)) {
if (node.firstChild === null || (node.firstChild.from <= pos && node.firstChild.to >= pos)) {
// it means the FunctionCallBody has no child, which means we are autocompleting the first parameter
result.push({ kind: ContextKind.Number });
break;
}
// at this point we are necessary autocompleting the second parameter
result.push({ kind: ContextKind.MetricName, metricName: '' }, { kind: ContextKind.Function }, { kind: ContextKind.Aggregation });
break;
}
// In all other cases, we are in the given situation:
// sum() or in rate() // sum() or in rate()
// with the cursor between the bracket. So we can autocomplete the metric, the function and the aggregation. // 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 }); result.push({ kind: ContextKind.MetricName, metricName: '' }, { kind: ContextKind.Function }, { kind: ContextKind.Aggregation });
@ -516,7 +550,11 @@ export class HybridComplete implements CompleteStrategy {
promQL(context: CompletionContext): Promise<CompletionResult | null> | CompletionResult | null { promQL(context: CompletionContext): Promise<CompletionResult | null> | CompletionResult | null {
const { state, pos } = context; const { state, pos } = context;
const tree = syntaxTree(state).resolve(pos, -1); const tree = syntaxTree(state).resolve(pos, -1);
const contexts = analyzeCompletion(state, tree); // The lines above can help you to print the current lezer tree.
// It's useful when you are trying to understand why it doesn't autocomplete.
// console.log(syntaxTree(state).topNode.toString());
// console.log(`current node: ${tree.type.name}`);
const contexts = analyzeCompletion(state, tree, pos);
let asyncResult: Promise<Completion[]> = Promise.resolve([]); let asyncResult: Promise<Completion[]> = Promise.resolve([]);
let completeSnippet = false; let completeSnippet = false;
let span = true; let span = true;