Handle autocomplete replacement better for more node types

Signed-off-by: Julius Volz <julius.volz@gmail.com>
This commit is contained in:
Julius Volz 2026-01-06 10:57:55 +01:00
parent b532eacae8
commit 3fc800410a
2 changed files with 80 additions and 17 deletions

View File

@ -638,7 +638,7 @@ describe('analyzeCompletion test', () => {
const state = createEditorState(value.expr);
const node = syntaxTree(state).resolve(value.pos, -1);
const result = analyzeCompletion(state, node, value.pos);
expect(value.expectedContext).toEqual(result);
expect(result).toEqual(value.expectedContext);
});
});
});
@ -861,7 +861,7 @@ describe('computeStartCompletePosition test', () => {
const state = createEditorState(value.expr);
const node = syntaxTree(state).resolve(value.pos, -1);
const result = computeStartCompletePosition(state, node, value.pos);
expect(value.expectedStart).toEqual(result);
expect(result).toEqual(value.expectedStart);
});
});
});
@ -911,16 +911,16 @@ describe('computeEndCompletePosition test', () => {
expectedEnd: 10, // should extend to end of 'sum_ov'
},
{
title: 'empty bracket - returns pos',
title: 'empty bracket - ends before the closing bracket',
expr: '{}',
pos: 1,
expectedEnd: 1,
},
{
title: 'cursor in label matchers - returns pos',
title: 'cursor in label matchers - ends before the closing bracket',
expr: 'metric_name{label="value"}',
pos: 12, // cursor after '{'
expectedEnd: 12,
expectedEnd: 25,
},
{
title: 'cursor in middle of label name in grouping clause - should extend to end',
@ -946,6 +946,54 @@ describe('computeEndCompletePosition test', () => {
pos: 17, // cursor after 'inst' (before 'ance')
expectedEnd: 26, // should extend to end of 'instance_name'
},
{
title: 'cursor in middle of function name rate - should extend to end',
expr: 'rate(foo[5m])',
pos: 2, // cursor after 'ra' (before 'te')
expectedEnd: 4, // should extend to end of 'rate'
},
{
title: 'cursor in middle of function name histogram_quantile - should extend to end',
expr: 'histogram_quantile(0.9, rate(foo[5m]))',
pos: 10, // cursor after 'histogram_' (before 'quantile')
expectedEnd: 18, // should extend to end of 'histogram_quantile'
},
{
title: 'cursor in middle of aggregator sum - should extend to end',
expr: 'sum(rate(foo[5m]))',
pos: 2, // cursor after 'su' (before 'm')
expectedEnd: 3, // should extend to end of 'sum'
},
{
title: 'cursor in middle of aggregator count_values - should extend to end',
expr: 'count_values("label", foo)',
pos: 6, // cursor after 'count_' (before 'values')
expectedEnd: 12, // should extend to end of 'count_values'
},
{
title: 'cursor in middle of nested function - should extend to end',
expr: 'sum(rate(foo[5m]))',
pos: 6, // cursor after 'ra' inside rate (before 'te')
expectedEnd: 8, // should extend to end of 'rate'
},
{
title: 'cursor at beginning of aggregator - should extend to end',
expr: 'avg by (instance) (rate(foo[5m]))',
pos: 1, // cursor after 'a' (before 'vg')
expectedEnd: 3, // should extend to end of 'avg'
},
{
title: 'cursor in middle of function name with binary op - should extend to end',
expr: 'rate(foo[5m]) / irate(bar[5m])',
pos: 17, // cursor after 'ir' inside irate (before 'ate')
expectedEnd: 21, // should extend to end of 'irate'
},
{
title: 'error node - returns pos (cursor position)',
expr: 'metric_name !',
pos: 13, // cursor at '!' (error node)
expectedEnd: 13, // error node returns pos
},
];
testCases.forEach((value) => {
it(value.title, () => {
@ -1398,7 +1446,7 @@ describe('autocomplete promQL test', () => {
expectedResult: {
options: [],
from: 10,
to: 10,
to: 11,
validFor: /^[a-zA-Z0-9_:]+$/,
},
},
@ -1409,7 +1457,7 @@ describe('autocomplete promQL test', () => {
expectedResult: {
options: [],
from: 10,
to: 10,
to: 12,
validFor: /^[a-zA-Z0-9_:]+$/,
},
},
@ -1564,7 +1612,7 @@ describe('autocomplete promQL test', () => {
const context = new CompletionContext(state, value.pos, true);
const completion = newCompleteStrategy(value.conf);
const result = await completion.promQL(context);
expect(value.expectedResult).toEqual(result);
expect(result).toEqual(value.expectedResult);
});
});

View File

@ -167,18 +167,33 @@ function arrayToCompletionResult(data: Completion[], from: number, to: number, i
}
// computeEndCompletePosition calculates the end position for autocompletion replacement.
// When the cursor is in the middle of an identifier (e.g., metric name) or label name, this ensures
// the entire token is replaced, not just the portion before the cursor. This fixes issue #15839.
// 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(state: EditorState, node: SyntaxNode, pos: number): number {
// For Identifier nodes (metric names) and LabelName nodes (label names in matchers,
// grouping clauses, etc.), extend the end position to include the entire token,
// even if the cursor is in the middle.
if (node.type.id === Identifier || node.type.id === LabelName) {
return node.to;
// For error nodes, use the cursor position as the end position
if (node.type.id === 0) {
return pos;
}
// Default: use the cursor position as the end position
return pos;
if (
node.type.id === LabelMatchers ||
node.type.id === GroupingLabels ||
node.type.id === FunctionCallBody ||
node.type.id === MatrixSelector ||
node.type.id === SubqueryExpr
) {
// When we're inside empty brackets, we want to replace up to just before the closing bracket.
return node.to - 1;
}
if (node.type.id === StringLiteral && (node.parent?.type.id === UnquotedLabelMatcher || node.parent?.type.id === QuotedLabelMatcher)) {
// For label values, we want to replace all content inside the quotes.
return node.parent.to - 1;
}
// For all other nodes, extend the end position to include the entire token.
return node.to;
}
// Matches complete PromQL durations, including compound units (e.g., 5m, 1d2h, 1h30m, etc.).