From 04a5a488b89ce6ba05b6657b509785d64c328b59 Mon Sep 17 00:00:00 2001 From: ADITYA TIWARI Date: Mon, 24 Nov 2025 17:51:14 +0000 Subject: [PATCH 1/7] fix: Suppress autocomplete for duration units when unit already present - No duration unit suggestions shown if a valid unit follows the digit (e.g. , ) - Adds related test cases Signed-off-by: ADITYA TIWARI --- .../src/complete/hybrid.test.ts | 34 +++++++++++++++++++ .../codemirror-promql/src/complete/hybrid.ts | 27 ++++++++++++++- 2 files changed, 60 insertions(+), 1 deletion(-) 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; From bf76fde0c8104de155f64022bf96c957bc5eb4ee Mon Sep 17 00:00:00 2001 From: ADITYA TIWARI <142050150+ADITYATIWARI342005@users.noreply.github.com> Date: Tue, 25 Nov 2025 17:56:35 +0530 Subject: [PATCH 2/7] Update duration regex for complete duration matching Refactor duration regex to match complete durations with units. Signed-off-by: ADITYA TIWARI <142050150+ADITYATIWARI342005@users.noreply.github.com> --- web/ui/module/codemirror-promql/src/complete/hybrid.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/web/ui/module/codemirror-promql/src/complete/hybrid.ts b/web/ui/module/codemirror-promql/src/complete/hybrid.ts index 429ac468dd..32d76956d8 100644 --- a/web/ui/module/codemirror-promql/src/complete/hybrid.ts +++ b/web/ui/module/codemirror-promql/src/complete/hybrid.ts @@ -166,17 +166,14 @@ 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('|')})$`); +// Matches complete duration with units (e.g., 5m, 30s, 1h, 500ms) +const durationWithUnitRegexp = new RegExp(`^\\d+(?:${durationTerms.map((term) => escapeRegExp(term.label)).join('|')})$`); function escapeRegExp(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } +// Determines if a duration already has a complete time unit to prevent autocomplete insertion (issue #15452) function hasCompleteDurationUnit(state: EditorState, node: SyntaxNode): boolean { if (node.from >= node.to) { return false; From 137f8465272432b771c33acee0fb208e33ff142b Mon Sep 17 00:00:00 2001 From: ADITYA TIWARI <142050150+ADITYATIWARI342005@users.noreply.github.com> Date: Tue, 25 Nov 2025 18:30:30 +0530 Subject: [PATCH 3/7] Add tests for durationWithUnitRegexp functionality Added tests for durationWithUnitRegexp to validate matching of complete durations with units and ensure non-matching cases are correctly identified. Signed-off-by: ADITYA TIWARI <142050150+ADITYATIWARI342005@users.noreply.github.com> --- .../src/complete/hybrid.test.ts | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) 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 25a2e8fb78..cc73161dce 100644 --- a/web/ui/module/codemirror-promql/src/complete/hybrid.test.ts +++ b/web/ui/module/codemirror-promql/src/complete/hybrid.test.ts @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { analyzeCompletion, computeStartCompletePosition, ContextKind } from './hybrid'; +import { analyzeCompletion, computeStartCompletePosition, ContextKind, durationWithUnitRegexp } from './hybrid'; import { createEditorState, mockedMetricsTerms, mockPrometheusServer } from '../test/utils-test'; import { Completion, CompletionContext } from '@codemirror/autocomplete'; import { @@ -642,6 +642,32 @@ describe('analyzeCompletion test', () => { }); }); +describe('durationWithUnitRegexp test', () => { + it('should match complete durations with units', () => { + const testCases = [ + { input: '5m', expected: true }, + { input: '30s', expected: true }, + { input: '1h', expected: true }, + { input: '500ms', expected: true }, + { input: '2d', expected: true }, + { input: '1w', expected: true }, + { input: '1y', expected: true }, + ]; + + testCases.forEach(({ input, expected }) => { + expect(durationWithUnitRegexp.test(input)).toBe(expected); + }); + }); + + it('should not match durations without units or partial units', () => { + const testCases = ['5', '30', '100', '5m5', 'm', 'd']; + + testCases.forEach((input) => { + expect(durationWithUnitRegexp.test(input)).toBe(false); + }); + }); +}); + describe('computeStartCompletePosition test', () => { const testCases = [ { From 3b098799d4729c79048e23532d5a2c75d84b586b Mon Sep 17 00:00:00 2001 From: ADITYA TIWARI <142050150+ADITYATIWARI342005@users.noreply.github.com> Date: Tue, 25 Nov 2025 18:31:10 +0530 Subject: [PATCH 4/7] Export durationWithUnitRegexp for external use Signed-off-by: ADITYA TIWARI <142050150+ADITYATIWARI342005@users.noreply.github.com> --- web/ui/module/codemirror-promql/src/complete/hybrid.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/ui/module/codemirror-promql/src/complete/hybrid.ts b/web/ui/module/codemirror-promql/src/complete/hybrid.ts index 32d76956d8..8a2d575552 100644 --- a/web/ui/module/codemirror-promql/src/complete/hybrid.ts +++ b/web/ui/module/codemirror-promql/src/complete/hybrid.ts @@ -167,7 +167,7 @@ function arrayToCompletionResult(data: Completion[], from: number, to: number, i } // Matches complete duration with units (e.g., 5m, 30s, 1h, 500ms) -const durationWithUnitRegexp = new RegExp(`^\\d+(?:${durationTerms.map((term) => escapeRegExp(term.label)).join('|')})$`); +export const durationWithUnitRegexp = new RegExp(`^\\d+(?:${durationTerms.map((term) => escapeRegExp(term.label)).join('|')})$`); function escapeRegExp(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); From 4fa435fad25598e355c85a3af1dcb3750085d4b8 Mon Sep 17 00:00:00 2001 From: ADITYA TIWARI Date: Tue, 25 Nov 2025 16:13:52 +0000 Subject: [PATCH 5/7] feat: use RegExp.escape polyfill for robust PromQL duration regex; add compound duration test cases Signed-off-by: ADITYA TIWARI --- .../src/complete/hybrid.test.ts | 15 +++++++++++-- .../codemirror-promql/src/complete/hybrid.ts | 22 ++++++++++++++----- 2 files changed, 30 insertions(+), 7 deletions(-) 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 cc73161dce..e958a8113b 100644 --- a/web/ui/module/codemirror-promql/src/complete/hybrid.test.ts +++ b/web/ui/module/codemirror-promql/src/complete/hybrid.test.ts @@ -652,11 +652,22 @@ describe('durationWithUnitRegexp test', () => { { input: '2d', expected: true }, { input: '1w', expected: true }, { input: '1y', expected: true }, + { input: '1d2h', expected: true }, + { input: '2h30m', expected: true }, + { input: '1h2m3s', expected: true }, + { input: '250ms2s', expected: true }, + { input: '2h3m4s5ms', expected: true }, + { input: '5', expected: false }, + { input: '5m5', expected: false }, + { input: 'm', expected: false }, + { input: 'd', expected: false }, + { input: '', expected: false }, + { input: '1hms', expected: false }, + { input: '2x', expected: false }, ]; - testCases.forEach(({ input, expected }) => { expect(durationWithUnitRegexp.test(input)).toBe(expected); - }); + }); }); it('should not match durations without units or partial units', () => { diff --git a/web/ui/module/codemirror-promql/src/complete/hybrid.ts b/web/ui/module/codemirror-promql/src/complete/hybrid.ts index 8a2d575552..36fb59be5b 100644 --- a/web/ui/module/codemirror-promql/src/complete/hybrid.ts +++ b/web/ui/module/codemirror-promql/src/complete/hybrid.ts @@ -166,13 +166,25 @@ function arrayToCompletionResult(data: Completion[], from: number, to: number, i } as CompletionResult; } -// Matches complete duration with units (e.g., 5m, 30s, 1h, 500ms) -export const durationWithUnitRegexp = new RegExp(`^\\d+(?:${durationTerms.map((term) => escapeRegExp(term.label)).join('|')})$`); - -function escapeRegExp(value: string): string { - return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +// Polyfill RegExp.escape for compatibility with ES2024 and TypeScript. +// Ensures safe, standards-based regex escaping in all environments. +declare global { + interface RegExpConstructor { + escape?: (s: string) => string; + } } +if (!RegExp.escape) { + RegExp.escape = function(s: string): string { + return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + }; +} + +// Matches complete PromQL durations, including compound units (e.g., 5m, 1d2h, 1h30m, etc.) +export const durationWithUnitRegexp = new RegExp( + `^(\\d+(${durationTerms.map(term => RegExp.escape!(term.label)).join('|')}))+$` +); + // Determines if a duration already has a complete time unit to prevent autocomplete insertion (issue #15452) function hasCompleteDurationUnit(state: EditorState, node: SyntaxNode): boolean { if (node.from >= node.to) { From 42418660d36e46bb835e957ce08826946257f127 Mon Sep 17 00:00:00 2001 From: ADITYA TIWARI Date: Tue, 25 Nov 2025 16:30:27 +0000 Subject: [PATCH 6/7] fix: lint errors in the files; move regex to one-line only Signed-off-by: ADITYA TIWARI --- web/ui/module/codemirror-promql/src/complete/hybrid.test.ts | 3 +-- web/ui/module/codemirror-promql/src/complete/hybrid.ts | 6 ++---- 2 files changed, 3 insertions(+), 6 deletions(-) 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 e958a8113b..587e9c5304 100644 --- a/web/ui/module/codemirror-promql/src/complete/hybrid.test.ts +++ b/web/ui/module/codemirror-promql/src/complete/hybrid.test.ts @@ -667,12 +667,11 @@ describe('durationWithUnitRegexp test', () => { ]; testCases.forEach(({ input, expected }) => { expect(durationWithUnitRegexp.test(input)).toBe(expected); - }); + }); }); it('should not match durations without units or partial units', () => { const testCases = ['5', '30', '100', '5m5', 'm', 'd']; - testCases.forEach((input) => { expect(durationWithUnitRegexp.test(input)).toBe(false); }); diff --git a/web/ui/module/codemirror-promql/src/complete/hybrid.ts b/web/ui/module/codemirror-promql/src/complete/hybrid.ts index 36fb59be5b..11d18adcef 100644 --- a/web/ui/module/codemirror-promql/src/complete/hybrid.ts +++ b/web/ui/module/codemirror-promql/src/complete/hybrid.ts @@ -175,15 +175,13 @@ declare global { } if (!RegExp.escape) { - RegExp.escape = function(s: string): string { + RegExp.escape = function (s: string): string { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }; } // Matches complete PromQL durations, including compound units (e.g., 5m, 1d2h, 1h30m, etc.) -export const durationWithUnitRegexp = new RegExp( - `^(\\d+(${durationTerms.map(term => RegExp.escape!(term.label)).join('|')}))+$` -); +export const durationWithUnitRegexp = new RegExp(`^(\\d+(${durationTerms.map((term) => RegExp.escape!(term.label)).join('|')}))+$`); // Determines if a duration already has a complete time unit to prevent autocomplete insertion (issue #15452) function hasCompleteDurationUnit(state: EditorState, node: SyntaxNode): boolean { From 49427cfcd2cf13366b2a27bd0069d58566b9c8fc Mon Sep 17 00:00:00 2001 From: ADITYA TIWARI <142050150+ADITYATIWARI342005@users.noreply.github.com> Date: Thu, 27 Nov 2025 16:40:33 +0530 Subject: [PATCH 7/7] Refactor duration regex and remove RegExp.escape polyfill Removed polyfill for RegExp.escape and updated duration regex. Signed-off-by: ADITYA TIWARI <142050150+ADITYATIWARI342005@users.noreply.github.com> --- .../codemirror-promql/src/complete/hybrid.ts | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/web/ui/module/codemirror-promql/src/complete/hybrid.ts b/web/ui/module/codemirror-promql/src/complete/hybrid.ts index 11d18adcef..814147e532 100644 --- a/web/ui/module/codemirror-promql/src/complete/hybrid.ts +++ b/web/ui/module/codemirror-promql/src/complete/hybrid.ts @@ -166,22 +166,9 @@ function arrayToCompletionResult(data: Completion[], from: number, to: number, i } as CompletionResult; } -// Polyfill RegExp.escape for compatibility with ES2024 and TypeScript. -// Ensures safe, standards-based regex escaping in all environments. -declare global { - interface RegExpConstructor { - escape?: (s: string) => string; - } -} - -if (!RegExp.escape) { - RegExp.escape = function (s: string): string { - return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - }; -} - -// Matches complete PromQL durations, including compound units (e.g., 5m, 1d2h, 1h30m, etc.) -export const durationWithUnitRegexp = new RegExp(`^(\\d+(${durationTerms.map((term) => RegExp.escape!(term.label)).join('|')}))+$`); +// Matches complete PromQL durations, including compound units (e.g., 5m, 1d2h, 1h30m, etc.). +// Duration units are a fixed, safe set (no regex metacharacters), so no escaping is needed. +export const durationWithUnitRegexp = new RegExp(`^(\\d+(${durationTerms.map((term) => term.label).join('|')}))+$`); // Determines if a duration already has a complete time unit to prevent autocomplete insertion (issue #15452) function hasCompleteDurationUnit(state: EditorState, node: SyntaxNode): boolean {