Repository: novuhq/novu
File: apps/dashboard/src/utils/liquid-autocomplete.ts (TypeScript)

Code before:
    const prefix = searchText.slice(0, -1);
    return variables.filter((v) => v.name.startsWith(prefix));
  }

  // Filter jit step namespaces out of the returned variables from the server
  const stepPayloadNamespaces = variables.reduce<string[]>((acc, variableItem) => {
    const match = variableItem.name.match(STEP_PAYLOAD_REGEX);

    if (match) {
      acc.push(match[0]);
    }

    return acc;
  }, []);

  // Create JIT variables based on the search text e.g. payload.foo, subscriber.data.foo, steps.digest-step.events[0].payload.foo
  const jitVariables = [PAYLOAD_NAMESPACE, SUBSCRIBER_DATA_NAMESPACE, ...stepPayloadNamespaces].reduce<
    LiquidVariable[]
  >((acc, namespace) => {
    if (searchText.startsWith(namespace) && searchText !== namespace) {
      // Ensure that if the user types payload.foo the first suggestion is payload.foo
      acc.push({ name: searchText, type: 'variable' });
    } else if (!searchText.startsWith(namespace)) {
      // For all other values, suggest payload.whatever, subscriber.data.whatever
      acc.push({
        name: `${namespace}.${searchLower.trim()}`,
        type: 'variable',
      });
    }

    return acc;
  }, []);

  // Guardrail to ensure
  const uniqueVariables = Array.from(
    new Map([...jitVariables, ...variables].map((item) => [item.name, item])).values()
  );

  // Show any variables containing the search text in the variable name (not the filters)
  return uniqueVariables.filter((v) => {
    const namePartWithoutFilters = v.name.split('|')[0].trim();
    return namePartWithoutFilters.toLowerCase().includes(searchLower);
  });
}

export function createAutocompleteSource(
  variables: LiquidVariable[],
  onVariableSelect?: (completion: Completion) => void
) {
  return (context: CompletionContext) => {
    // Match text that starts with {{ and capture everything after it until the cursor position

Review comment (bug):
we shouldn't use `searchLower` as in the result it will suggest me wrong variable name; for example I have variable in my payload `firstName`, so I typed `{{firstName}}`, but in the dropdown it suggests `payload.firstname`

![Screenshot 2025-05-08 at 10 05 16](https://github.com/user-attachments/assets/59669a0e-3386-459d-996e-38c11cc29983)

Code after:
    const prefix = searchText.slice(0, -1);
    return variables.filter((v) => v.name.startsWith(prefix));
  }

  // Filter jit step namespaces out of the returned variables from the server
  const stepPayloadNamespaces = variables.reduce<string[]>((acc, variableItem) => {
    const match = variableItem.name.match(STEP_PAYLOAD_REGEX);

    const withPayload = match ? `${match[0]}.payload` : null;

    if (withPayload && !acc.includes(withPayload)) {
      acc.push(withPayload);
    }

    return acc;
  }, []);

  // Create JIT variables based on the search text e.g. payload.foo, subscriber.data.foo, steps.digest-step.events[0].payload.foo
  const jitVariables = [PAYLOAD_NAMESPACE, SUBSCRIBER_DATA_NAMESPACE, ...stepPayloadNamespaces].reduce<
    LiquidVariable[]
  >((acc, namespace) => {
    // If the user is typing steps.*, don't suggest any variables like payload.steps.digest-step.events
    if (searchText.startsWith('steps.')) {
      return acc;
    }

    if (searchText.startsWith(namespace) && searchText !== namespace) {
      // Ensure that if the user types payload.foo the first suggestion is payload.foo
      acc.push({ name: searchText, type: 'variable' });
    } else if (!searchText.startsWith(namespace)) {
      // For all other values, suggest payload.whatever, subscriber.data.whatever
      acc.push({
        name: `${namespace}.${searchText.trim()}`,
        type: 'variable',
      });
    }

    return acc;
  }, []);

  // Guardrail to ensure
  const uniqueVariables = Array.from(
    new Map([...jitVariables, ...variables].map((item) => [item.name, item])).values()
  );

  // Show any variables containing the search text in the variable name (not the filters)
  return uniqueVariables.filter((v) => {
    const namePartWithoutFilters = v.name.split('|')[0].trim();
    return namePartWithoutFilters.includes(searchTextTrimmed);
  });
}