Merge pull request #18894 from MichaHoffmann/mhoffmann/codemirror-fix-autocomplete-for-functions
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 for 32-bit x86 (push) Waiting to run
CI / Go tests for Prometheus upgrades and downgrades (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 / Compliance testing (push) Waiting to run
CI / Build Prometheus for common architectures (push) Waiting to run
CI / Build Prometheus for all architectures (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
govulncheck / Run govulncheck (push) Waiting to run
Scorecards supply-chain security / Scorecards analysis (push) Waiting to run

codemirror: fix autocomplete edgecase for functions
This commit is contained in:
George Krajcsovits 2026-06-10 09:47:27 +02:00 committed by GitHub
commit a9564ef219
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 116 additions and 4 deletions

View file

@ -74,6 +74,31 @@ describe('analyzeCompletion test', () => {
{ kind: ContextKind.Aggregation },
],
},
{
title: 'autocomplete AggregateOpModifier or BinOp after closing aggregation',
expr: 'sum()',
pos: 5, // cursor is after the closing bracket
expectedContext: [{ kind: ContextKind.AggregateOpModifier }, { kind: ContextKind.BinOp }],
},
{
title: 'metric/function/aggregation autocompletion in incomplete function',
expr: 'sum(',
pos: 4,
expectedContext: [
{
kind: ContextKind.MetricName,
metricName: '',
},
{ kind: ContextKind.Function },
{ kind: ContextKind.Aggregation },
],
},
{
title: 'autocomplete binOp after closing function',
expr: 'rate(foo[5m])',
pos: 13, // cursor is after the closing bracket
expectedContext: [{ kind: ContextKind.BinOp }],
},
{
title: 'metric/function/aggregation autocompletion 2',
expr: 'sum(rat)',
@ -100,6 +125,18 @@ describe('analyzeCompletion test', () => {
{ kind: ContextKind.Aggregation },
],
},
{
title: 'autocomplete AggregateOpModifier or BinOp after closing nested aggregation',
expr: 'sum(rate(foo[5m]))',
pos: 18, // cursor is after the closing bracket
expectedContext: [{ kind: ContextKind.AggregateOpModifier }, { kind: ContextKind.BinOp }],
},
{
title: 'autocomplete binOp after closing aggregation with existing modifier',
expr: 'sum by(job)(rate(foo[5m]))',
pos: 26, // cursor is after the closing bracket
expectedContext: [{ kind: ContextKind.BinOp }],
},
{
title: 'metric/function/aggregation autocompletion 4',
expr: 'sum(rate(my_))',
@ -717,6 +754,18 @@ describe('computeStartCompletePosition test', () => {
pos: 9, // cursor is between the bracket
expectedStart: 9,
},
{
title: 'start should be equal to the pos after closing function',
expr: 'rate(foo[5m])',
pos: 13, // cursor is after the closing bracket
expectedStart: 13,
},
{
title: 'start should be equal to the pos after closing aggregation',
expr: 'sum(rate(foo[5m]))',
pos: 18, // cursor is after the closing bracket
expectedStart: 18,
},
{
title: 'bracket containing a substring',
expr: '{myL}',
@ -994,12 +1043,24 @@ describe('computeEndCompletePosition test', () => {
pos: 13, // cursor at '!' (error node)
expectedEnd: 13, // error node returns pos
},
{
title: 'end should be equal to the pos after closing function',
expr: 'rate(foo[5m])',
pos: 13, // cursor is after the closing bracket
expectedEnd: 13,
},
{
title: 'end should be equal to the pos after closing aggregation',
expr: 'sum(rate(foo[5m]))',
pos: 18, // cursor is after the closing bracket
expectedEnd: 18,
},
];
testCases.forEach((value) => {
it(value.title, () => {
const state = createEditorState(value.expr);
const node = syntaxTree(state).resolve(value.pos, -1);
const result = computeEndCompletePosition(node, value.pos);
const result = computeEndCompletePosition(state, node, value.pos);
expect(result).toEqual(value.expectedEnd);
});
});
@ -1043,6 +1104,28 @@ describe('autocomplete promQL test', () => {
validFor: /^[a-zA-Z0-9_:]+$/,
},
},
{
title: 'offline autocomplete aggregate operation modifier or binary operator after closing aggregation',
expr: 'sum()',
pos: 5, // cursor is after the closing bracket
expectedResult: {
options: ([] as Completion[]).concat(aggregateOpModifierTerms, binOpTerms),
from: 5,
to: 5,
validFor: /^[a-zA-Z0-9_:]+$/,
},
},
{
title: 'offline autocomplete binary operator after closing function',
expr: 'rate(foo[5m])',
pos: 13, // cursor is after the closing bracket
expectedResult: {
options: binOpTerms,
from: 13,
to: 13,
validFor: /^[a-zA-Z0-9_:]+$/,
},
},
{
title: 'offline function/aggregation autocompletion in aggregation 2',
expr: 'sum(ra)',
@ -1087,6 +1170,17 @@ describe('autocomplete promQL test', () => {
validFor: /^[a-zA-Z0-9_:]+$/,
},
},
{
title: 'offline autocomplete aggregate operation modifier or binary operator after closing nested aggregation',
expr: 'sum(rate(foo[5m]))',
pos: 18, // cursor is after the closing bracket
expectedResult: {
options: ([] as Completion[]).concat(aggregateOpModifierTerms, binOpTerms),
from: 18,
to: 18,
validFor: /^[a-zA-Z0-9_:]+$/,
},
},
{
title: 'offline function/aggregation autocompletion in aggregation 4',
expr: 'sum by (instance, job) ( sum_over(scrape_series_added[1h])) / sum by (instance, job) (sum_over_time(scrape_samples_scraped[1h])) > 0.1 and sum by(instance, job) (scrape_samples_scraped{) > 100',

View file

@ -17,6 +17,7 @@ import { PrometheusClient } from '../client';
import {
Add,
AggregateExpr,
AggregateModifier,
And,
BinaryExpr,
BoolModifier,
@ -175,16 +176,24 @@ function escapePromQLString(str: string): string {
return str.replace(/([\\"])/g, '\\$1');
}
function isAfterClosedFunctionCallBody(state: EditorState, node: SyntaxNode, pos: number): boolean {
return node.type.id === FunctionCallBody && pos >= node.to && node.from < node.to && state.sliceDoc(node.to - 1, node.to) === ')';
}
// computeEndCompletePosition calculates the end position for autocompletion replacement.
// When the cursor is in the middle of a token, this ensures the entire token is replaced,
// not just the portion before the cursor. This fixes issue #15839.
// Note: this method is exported only for testing purpose.
export function computeEndCompletePosition(node: SyntaxNode, pos: number): number {
export function computeEndCompletePosition(state: EditorState, node: SyntaxNode, pos: number): number {
// For error nodes, use the cursor position as the end position
if (node.type.id === 0) {
return pos;
}
if (isAfterClosedFunctionCallBody(state, node, pos)) {
return pos;
}
if (
node.type.id === LabelMatchers ||
node.type.id === GroupingLabels ||
@ -240,7 +249,9 @@ function computeStartCompleteLabelPositionInLabelMatcherOrInGroupingLabel(node:
export function computeStartCompletePosition(state: EditorState, node: SyntaxNode, pos: number): number {
const currentText = state.doc.slice(node.from, pos).toString();
let start = node.from;
if (node.type.id === LabelMatchers || node.type.id === GroupingLabels) {
if (isAfterClosedFunctionCallBody(state, node, pos)) {
start = pos;
} else if (node.type.id === LabelMatchers || node.type.id === GroupingLabels) {
start = computeStartCompleteLabelPositionInLabelMatcherOrInGroupingLabel(node, pos);
} else if (
(node.type.id === FunctionCallBody && node.firstChild === null) ||
@ -545,6 +556,13 @@ export function analyzeCompletion(state: EditorState, node: SyntaxNode, pos: num
result.push({ kind: ContextKind.Duration });
break;
case FunctionCallBody:
if (isAfterClosedFunctionCallBody(state, node, pos)) {
if (node.parent?.type.id === AggregateExpr && !containsAtLeastOneChild(node.parent, AggregateModifier)) {
result.push({ kind: ContextKind.AggregateOpModifier });
}
result.push({ kind: ContextKind.BinOp });
break;
}
// 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.
@ -711,7 +729,7 @@ export class HybridComplete implements CompleteStrategy {
return arrayToCompletionResult(
result,
computeStartCompletePosition(state, tree, pos),
computeEndCompletePosition(tree, pos),
computeEndCompletePosition(state, tree, pos),
completeSnippet,
span
);