import {Fragment, useMemo} from 'react'; import {useTheme} from '@emotion/react'; import styled from '@emotion/styled'; import {Tooltip} from 'sentry/components/tooltip'; import {TokenType} from 'sentry/views/metrics/formulaParser/types'; import grammar from './formulaFormatting.pegjs'; const operatorTokens = new Set([ TokenType.PLUS, TokenType.MINUS, TokenType.MULTIPLY, TokenType.DIVIDE, ]); interface EquationFormatterProps { equation: string; errors?: { message: string; start: number; end?: number; }[]; } export function EquationFormatter({equation: formula, errors}: EquationFormatterProps) { const theme = useTheme(); const tokens = useMemo(() => { try { return grammar.parse(formula); } catch (err) { return undefined; } }, [formula]); if (!tokens) { // If the formula cannot be parsed, we simply return it without any highlighting return {formula}; } const findMatchingError = (charCount: number) => { if (!errors || errors.length === 0) { return null; } return errors.find( error => error.start <= charCount && (!error.end || error.end >= charCount) ); }; let charCount = 0; let hasActiveTooltip = false; const renderedTokens = ( {tokens.map((token, index) => { const error = findMatchingError(charCount); charCount += token.content.length; if (error) { const content = ( {token.content} ); // Only show one tooltip at a time const showTooltip = !hasActiveTooltip; hasActiveTooltip = true; return showTooltip ? ( {content} ) : ( content ); } if (token.type === TokenType.VARIABLE) { return ( {token.content} ); } if (operatorTokens.has(token.type)) { return ( {token.content} ); } return ( {token.content} ); })} ); // Unexpected EOL might not match a token const remainingError = !hasActiveTooltip && findMatchingError(charCount); return ( {renderedTokens} {remainingError && (   )} ); } const Token = styled('span')` white-space: pre; `;