Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 | 76x 76x 76x 76x 76x 76x 380x 12x 12x 60x 29x 29x 29x 29x 2x 2x 29x 29x 27x 2x 29x 267x 9x 2x | // Frontend aggregate grammar for property-key formulas:
// FUNCTION(@property_key)
//
// FUNCTION is one of SUM/AVG/MIN/MAX/COUNT. property_key is the stable
// object-schema key, including dotted keys such as "sink.heat".
const AGGREGATE_FUNCTIONS = ["SUM", "AVG", "MIN", "MAX", "COUNT"] as const;
const AGGREGATE_KEY_PATTERN = "[A-Za-z_][A-Za-z0-9_.]*";
/**
* `react-mentions` custom triggers must expose:
* - group 1: the text range to replace
* - group 2: the query used to filter suggestions
*/
export const AGGREGATE_FUNCTION_TRIGGER = /(?:^|[^\w@.])(([A-Za-z]{1,5}))$/;
export const AGGREGATE_KEY_TRIGGER = new RegExp(
`\\b(?:${AGGREGATE_FUNCTIONS.join("|")})\\s*\\(\\s*(@((${AGGREGATE_KEY_PATTERN})?))$`,
"i",
);
/**
* Temporary react-mentions markup for aggregate snippets.
*
* Aggregate selections must persist as plain formula text, not as property
* mentions. A bare "__id__" markup looks tempting, but react-mentions parses
* that as "any text is a mention", which breaks ordinary `@Pump` suggestions.
*/
export const AGGREGATE_SUGGESTION_MARKUP = "{{__id__}}";
const AGGREGATE_SUGGESTION_MARKUP_PATTERN = /\{\{([^}]+)\}\}/g;
export type AggregateSuggestion = {
id: string;
display: string;
};
export type AggregateQueryRange = {
query: string;
start: number;
end: number;
};
export const aggregateFunctionOptions: AggregateSuggestion[] =
AGGREGATE_FUNCTIONS.map((functionName) => ({
id: `${functionName}(@`,
display: `${functionName}(@key)`,
}));
/**
* Return aggregate function snippets matching the partial token immediately
* before the caret. The inserted text intentionally includes "(@" so choosing
* a function moves the user straight into property-key completion.
*/
export function getAggregateFunctionOptions(
query: string,
): AggregateSuggestion[] {
const normalizedQuery = query.toUpperCase();
return aggregateFunctionOptions.filter(({ id }) =>
id.startsWith(normalizedQuery),
);
}
export function isCursorInAggregateKeyContext(
value: string,
cursorPosition: number,
): boolean {
return getAggregateKeyQuery(value, cursorPosition) !== null;
}
/**
* Detect the property-key portion of calls like SUM(@work_mechanical) or
* SUM(@sink.heat). The returned range is expressed in plain-text input
* coordinates, not react-mentions markup coordinates.
*/
export function getAggregateKeyQuery(
value: string,
cursorPosition: number,
): AggregateQueryRange | null {
const prefix = value.slice(0, cursorPosition);
const match = prefix.match(
new RegExp(
`\\b(?:${AGGREGATE_FUNCTIONS.join("|")})\\s*\\(\\s*@(${AGGREGATE_KEY_PATTERN})?$`,
"i",
),
);
if (!match) return null;
const query = match[1] ?? "";
return {
query,
start: cursorPosition - query.length,
end: cursorPosition,
};
}
/**
* Detect a partial aggregate function name without stealing ordinary property
* mentions or dotted property-key completions from react-mentions.
*/
export function getAggregateFunctionQuery(
value: string,
cursorPosition: number,
): AggregateQueryRange | null {
const prefix = value.slice(0, cursorPosition);
const match = prefix.match(/(?:^|[^\w@.])([A-Za-z]{1,5})$/);
if (!match || getAggregateFunctionOptions(match[1]).length === 0) {
return null;
}
return {
query: match[1],
start: cursorPosition - match[1].length,
end: cursorPosition,
};
}
export function isCursorInAggregateFunctionContext(
value: string,
cursorPosition: number,
): boolean {
return getAggregateFunctionQuery(value, cursorPosition) !== null;
}
/**
* Complete a selected property key and close the aggregate call, while avoiding
* a duplicate ")" when replacing text before an existing close parenthesis.
*/
export function getAggregateKeyInsertionText(
replacement: string,
value: string,
queryEnd: number,
): string {
return value[queryEnd] === ")" ? replacement : `${replacement})`;
}
export function unwrapAggregateSuggestionMarkup(value: string): string {
return value.replace(
AGGREGATE_SUGGESTION_MARKUP_PATTERN,
(_match, insertedText: string) => insertedText,
);
}
|