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

Code before:
      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
    const word = context.matchBefore(/\{\{([^}]*)/);
    if (!word) return null;

    const options = completions(variables)(context);
    if (!options) return null;

    const { from, to } = options;

    return {

Review comment (suggestion):
these doesn't seem to be correct suggestions: 

here second variable, and last two
![Screenshot 2025-05-08 at 10 09 31](https://github.com/user-attachments/assets/c1ebb9e3-064d-42d4-8a63-810dc7463b8c)

and in this one all them:
![Screenshot 2025-05-08 at 10 08 03](https://github.com/user-attachments/assets/f6f32776-10bd-4941-9c8d-fa85613396f1)


Code after:

    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);
  });
}

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
    const word = context.matchBefore(/\{\{([^}]*)/);
    if (!word) return null;