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

Code before:
  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;
}

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

Review comment (security):
I would say we can simplify this thing by a single const

```typescript
const durationWithUnitRegexp = new RegExp(`^\\d+(?:${durationTerms.map((term) => term.label).map((label) => escapeRegExp(label)).join('|')})$`);
```

And it will be nice to put in comment the expected result and have a test actually.

Code after:
  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.
// 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.