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 FormularFormatterProps {
formula: string;
errors?: {
message: string;
start: number;
end?: number;
}[];
}
export function FormularFormatter({formula, errors}: FormularFormatterProps) {
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;
`;