diff --git a/web/ui/module/codemirror-promql/src/complete/hybrid.test.ts b/web/ui/module/codemirror-promql/src/complete/hybrid.test.ts index 526a5ce4f8..25a2e8fb78 100644 --- a/web/ui/module/codemirror-promql/src/complete/hybrid.test.ts +++ b/web/ui/module/codemirror-promql/src/complete/hybrid.test.ts @@ -559,6 +559,18 @@ describe('analyzeCompletion test', () => { pos: 28, expectedContext: [{ kind: ContextKind.Duration }], }, + { + title: 'do not autocomplete duration when unit already present in matrixSelector', + expr: 'rate(foo[5m])', + pos: 10, + expectedContext: [], + }, + { + title: 'do not autocomplete duration when multi char unit already present in matrixSelector', + expr: 'rate(foo[5ms])', + pos: 10, + expectedContext: [], + }, { title: 'autocomplete duration for a subQuery', expr: 'go[5d:5]', @@ -1229,6 +1241,28 @@ describe('autocomplete promQL test', () => { validFor: undefined, }, }, + { + title: 'offline do not autocomplete duration when unit already present in matrixSelector', + expr: 'rate(foo[5m])', + pos: 10, + expectedResult: { + options: [], + from: 10, + to: 10, + validFor: /^[a-zA-Z0-9_:]+$/, + }, + }, + { + title: 'offline do not autocomplete duration when multi char unit already present in matrixSelector', + expr: 'rate(foo[5ms])', + pos: 10, + expectedResult: { + options: [], + from: 10, + to: 10, + validFor: /^[a-zA-Z0-9_:]+$/, + }, + }, { title: 'offline autocomplete duration for a subQuery', expr: 'go[5d:5]', diff --git a/web/ui/module/codemirror-promql/src/complete/hybrid.ts b/web/ui/module/codemirror-promql/src/complete/hybrid.ts index 76efc34442..429ac468dd 100644 --- a/web/ui/module/codemirror-promql/src/complete/hybrid.ts +++ b/web/ui/module/codemirror-promql/src/complete/hybrid.ts @@ -166,6 +166,25 @@ function arrayToCompletionResult(data: Completion[], from: number, to: number, i } as CompletionResult; } +const durationUnitLabels = durationTerms + .map((term) => term.label) + .filter((label): label is string => typeof label === 'string') + .sort((a, b) => b.length - a.length); + +const durationWithUnitRegexp = new RegExp(`^\\d+(?:${durationUnitLabels.map((label) => escapeRegExp(label)).join('|')})$`); + +function escapeRegExp(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +function hasCompleteDurationUnit(state: EditorState, node: SyntaxNode): boolean { + if (node.from >= node.to) { + return false; + } + const nodeContent = state.sliceDoc(node.from, node.to); + return durationWithUnitRegexp.test(nodeContent); +} + // computeStartCompleteLabelPositionInLabelMatcherOrInGroupingLabel calculates the start position only when the node is a LabelMatchers or a GroupingLabels function computeStartCompleteLabelPositionInLabelMatcherOrInGroupingLabel(node: SyntaxNode, pos: number): number { // Here we can have two different situations: @@ -477,12 +496,18 @@ export function analyzeCompletion(state: EditorState, node: SyntaxNode, pos: num // Duration, Duration, ⚠(NumberLiteral) // ) // So we should continue to autocomplete a duration - result.push({ kind: ContextKind.Duration }); + if (!hasCompleteDurationUnit(state, node)) { + result.push({ kind: ContextKind.Duration }); + } } else { result.push({ kind: ContextKind.Number }); } break; case NumberDurationLiteralInDurationContext: + if (!hasCompleteDurationUnit(state, node)) { + result.push({ kind: ContextKind.Duration }); + } + break; case OffsetExpr: result.push({ kind: ContextKind.Duration }); break;