Repository: prometheus/prometheus
File: web/ui/module/codemirror-promql/src/complete/hybrid.ts (TypeScript)

Code before:
  }
  return state.sliceDoc(currentNode.from, currentNode.to);
}

function arrayToCompletionResult(data: Completion[], from: number, to: number, includeSnippet = false, span = true): CompletionResult {
  const options = data;
  if (includeSnippet) {
    options.push(...snippets);
  }
  return {
    from: from,
    to: to,
    options: options,
    validFor: span ? /^[a-zA-Z0-9_:]+$/ : undefined,
  } 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('|')}))+$`);

// 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;
  }
  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:
  // 1. `metric{}` or `sum by()` with the cursor between the bracket
  // and so we have increment the starting position to avoid to consider the open bracket when filtering the autocompletion list.
  // 2. `metric{foo="bar",} or `sum by(foo,)  with the cursor after the comma.
  // Then the start number should be the current position to avoid to consider the previous labelMatcher/groupingLabel when filtering the autocompletion list.
  let start = node.from + 1;

Review comment (security):
Sorry, I didn't think that this would introduce so much trouble - if we got to define `escape` manually anyway in case it doesn't exist, then the previous solution was simpler.

But maybe the even better alternative is to just not escape anything, given that durations should never contain special characters?

Code after:
  }
  return state.sliceDoc(currentNode.from, currentNode.to);
}

function arrayToCompletionResult(data: Completion[], from: number, to: number, includeSnippet = false, span = true): CompletionResult {
  const options = data;
  if (includeSnippet) {
    options.push(...snippets);
  }
  return {
    from: from,
    to: to,
    options: options,
    validFor: span ? /^[a-zA-Z0-9_:]+$/ : undefined,
  } as CompletionResult;
}

// 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 {
  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:
  // 1. `metric{}` or `sum by()` with the cursor between the bracket
  // and so we have increment the starting position to avoid to consider the open bracket when filtering the autocompletion list.
  // 2. `metric{foo="bar",} or `sum by(foo,)  with the cursor after the comma.
  // Then the start number should be the current position to avoid to consider the previous labelMatcher/groupingLabel when filtering the autocompletion list.
  let start = node.from + 1;
  if (node.firstChild !== null) {
    // here that means the LabelMatchers / GroupingLabels has a child, which is not possible if we have the expression `metric{}`. So we are likely trying to autocomplete the label list after a comma
    start = pos;
  }
  return start;
}

// computeStartCompletePosition calculates the start position of the autocompletion.
// It is an important step because the start position will be used by CMN to find the string and then to use it to filter the CompletionResult.
// A wrong `start` position will lead to have the completion not working.
// Note: this method is exported only for testing purpose.
export function computeStartCompletePosition(state: EditorState, node: SyntaxNode, pos: number): number {
  const currentText = state.doc.slice(node.from, pos).toString();