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

Code before:
    return '';
  }
  currentNode = currentNode.getChild(Identifier);
  if (!currentNode) {
    return '';
  }
  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 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, '\\$&');
}

// 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;

Review comment (security):
Can we use `RegExp.escape()` (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/escape) here? Or actually, since we know that every duration term is only a single letter, maybe we are ok without explicit escaping in the first place?

Code after:
    return '';
  }
  currentNode = currentNode.getChild(Identifier);
  if (!currentNode) {
    return '';
  }
  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.