Macros 2.0 (v0.7.3) - Variable Shorthand: Comparison Operators & Autocomplete Improvements (#5050)

* feat: Add numeric comparison operators (>, >=, <, <=) to variable shorthand syntax

- Added four new comparison operators for local and global variables
- Implemented greaterThan, greaterThanOrEqual, lessThan, lessThanOrEqual operations in MacroCstWalker
- Updated lexer to recognize >=, >, <=, < operators (longer patterns before shorter to avoid conflicts)
- Simplified parser by consolidating operator alternatives into single OR block
- Enhanced autocomplete with operator definitions, examples, and usage

* Improve autocomplete for variable shorthand operators with cursor-aware filtering

- Track `variableNameEnd` and `variableOperatorEnd` positions in parsed macro context for accurate cursor position checks
- Add `isShortOperatorPrefix()` helper to detect operators that could be prefixes of longer ones (e.g., `>` → `>=`)
- Fix operator autocomplete to show longer variants when typing short operators (typing `>` now shows both `>` and `>=`)
- Show operator suggestions immediately when variable
This commit is contained in:
Wolfsblvt
2026-01-23 00:05:00 +01:00
committed by GitHub
parent 6f5032f20e
commit 42155ecebf
6 changed files with 323 additions and 97 deletions
@@ -39,7 +39,9 @@ import { onboardingExperimentalMacroEngine } from '../macros/engine/MacroDiagnos
* @property {boolean} isVariableShorthand - Whether this is a variable shorthand (starts with . or $).
* @property {'.'|'$'|null} variablePrefix - The variable prefix (. for local, $ for global), or null.
* @property {string} variableName - The variable name being typed (after the prefix).
* @property {number} variableNameEnd - The end of the variable name (for partial matches).
* @property {string|null} variableOperator - The operator typed (=, ++, --, +=), or null.
* @property {number} variableOperatorEnd - The end of the variable operator (for partial matches).
* @property {string} variableValue - The value after the operator (for = and +=).
* @property {boolean} isTypingVariableName - Whether cursor is in the variable name area.
* @property {boolean} isTypingOperator - Whether cursor is at/after variable name, ready for operator.
@@ -499,13 +501,13 @@ export const VariableShorthandDefinitions = new Map([
type: VariableShorthandType.LOCAL,
name: 'Local Variable',
description: 'Access or modify a local variable (scoped to current chat).',
operations: ['get', 'set (=)', 'increment (++)', 'decrement (--)', 'add (+=)', 'subtract (-=)', 'logical or (||)', 'nullish coalescing (??)', 'logical or assign (||=)', 'nullish coalescing assign (??=)', 'equals (==)', 'not equals (!=)'],
operations: ['get', 'set (=)', 'increment (++)', 'decrement (--)', 'add (+=)', 'subtract (-=)', 'logical or (||)', 'nullish coalescing (??)', 'logical or assign (||=)', 'nullish coalescing assign (??=)', 'equals (==)', 'not equals (!=)', 'greater than (>)', 'greater than or equal (>=)', 'less than (<)', 'less than or equal (<=)'],
}],
[VariableShorthandType.GLOBAL, {
type: VariableShorthandType.GLOBAL,
name: 'Global Variable',
description: 'Access or modify a global variable (shared across all chats).',
operations: ['get', 'set (=)', 'increment (++)', 'decrement (--)', 'add (+=)', 'subtract (-=)', 'logical or (||)', 'nullish coalescing (??)', 'logical or assign (||=)', 'nullish coalescing assign (??=)', 'equals (==)', 'not equals (!=)'],
operations: ['get', 'set (=)', 'increment (++)', 'decrement (--)', 'add (+=)', 'subtract (-=)', 'logical or (||)', 'nullish coalescing (??)', 'logical or assign (||=)', 'nullish coalescing assign (??=)', 'equals (==)', 'not equals (!=)', 'greater than (>)', 'greater than or equal (>=)', 'less than (<)', 'less than or equal (<=)'],
}],
]);
@@ -633,6 +635,10 @@ export class VariableShorthandAutoCompleteOption extends AutoCompleteOption {
`{{${prefix}myvar ??= value}} - Set if undefined, get value`,
`{{${prefix}myvar == test}} - Compare (returns true/false)`,
`{{${prefix}myvar != test}} - Compare not equal (returns true/false)`,
`{{${prefix}score > 10}} - Greater than (numeric, returns true/false)`,
`{{${prefix}score >= 10}} - Greater than or equal (numeric)`,
`{{${prefix}score < 10}} - Less than (numeric, returns true/false)`,
`{{${prefix}score <= 10}} - Less than or equal (numeric)`,
];
for (const ex of examples) {
const li = document.createElement('li');
@@ -810,6 +816,10 @@ export class VariableNameAutoCompleteOption extends AutoCompleteOption {
`{{${prefix}${this.#varName} ??= value}} - Set if undefined, get value`,
`{{${prefix}${this.#varName} == test}} - Compare (returns true/false)`,
`{{${prefix}${this.#varName} != test}} - Compare not equal (returns true/false)`,
`{{${prefix}${this.#varName} > 10}} - Greater than (numeric)`,
`{{${prefix}${this.#varName} >= 10}} - Greater than or equal (numeric)`,
`{{${prefix}${this.#varName} < 10}} - Less than (numeric)`,
`{{${prefix}${this.#varName} <= 10}} - Less than or equal (numeric)`,
];
for (const ex of examples) {
const li = document.createElement('li');
@@ -823,6 +833,18 @@ export class VariableNameAutoCompleteOption extends AutoCompleteOption {
}
}
/**
* Checks if an operator is a short one that could be a prefix of a longer operator.
* For example, '>' is a prefix of '>=', '<' is a prefix of '<='.
* @param {string} op - The operator to check.
* @returns {boolean} True if the operator could be a prefix of a longer operator.
*/
function isShortOperatorPrefix(op) {
// These operators could have longer variants typed after them
const shortPrefixes = ['>', '<', '=', '|', '?', '+', '-', '!'];
return shortPrefixes.includes(op);
}
/**
* Variable shorthand operators with metadata.
* @type {Map<string, { symbol: string, name: string, description: string, needsValue: boolean }>}
@@ -894,6 +916,30 @@ export const VariableOperatorDefinitions = new Map([
description: 'Compare the variable value to another value. Returns "true" if not equal, "false" if equal.',
needsValue: true,
}],
['>', {
symbol: '>',
name: 'Greater Than',
description: 'Numeric comparison. Returns "true" if variable is greater than value, "false" otherwise.',
needsValue: true,
}],
['>=', {
symbol: '>=',
name: 'Greater Than or Equal',
description: 'Numeric comparison. Returns "true" if variable is greater than or equal to value, "false" otherwise.',
needsValue: true,
}],
['<', {
symbol: '<',
name: 'Less Than',
description: 'Numeric comparison. Returns "true" if variable is less than value, "false" otherwise.',
needsValue: true,
}],
['<=', {
symbol: '<=',
name: 'Less Than or Equal',
description: 'Numeric comparison. Returns "true" if variable is less than or equal to value, "false" otherwise.',
needsValue: true,
}],
]);
/**
@@ -1224,19 +1270,35 @@ export function parseMacroContext(macroText, cursorOffset) {
} else if (operatorText.startsWith('!=')) {
variableOperator = '!=';
i += 2;
} else if (operatorText.startsWith('>=')) {
variableOperator = '>=';
i += 2;
} else if (operatorText.startsWith('>')) {
variableOperator = '>';
i += 1;
} else if (operatorText.startsWith('<=')) {
variableOperator = '<=';
i += 2;
} else if (operatorText.startsWith('<')) {
variableOperator = '<';
i += 1;
} else if (operatorText.startsWith('=')) {
variableOperator = '=';
i += 1;
} else if (operatorText.startsWith('+') || operatorText.startsWith('-') || operatorText.startsWith('|') || operatorText.startsWith('?') || operatorText.startsWith('!')) {
} else if (operatorText.startsWith('+') || operatorText.startsWith('-') || operatorText.startsWith('|') || operatorText.startsWith('?') || operatorText.startsWith('!') || operatorText.startsWith('>') || operatorText.startsWith('<')) {
// Partial operator prefix - user is typing an operator
partialOperator = operatorText[0];
} else if (operatorText.length > 0 && !/^\s/.test(operatorText)) {
} else if (operatorText.length > 0 && !/^\s/.test(operatorText) && !operatorText.startsWith('}')) {
// There's non-whitespace after the variable name that isn't a valid operator
// This is an invalid trailing character (e.g., $my$ or .var@test)
// Exception: } is the closing brace, not an invalid char
hasInvalidTrailingChars = true;
invalidTrailingChars = operatorText.trim();
}
// Track where the operator ends (for cursor position checks)
const variableOperatorEnd = i;
// Check if operator requires a value
const operatorDef = variableOperator ? VariableOperatorDefinitions.get(variableOperator) : null;
const operatorNeedsValue = operatorDef?.needsValue ?? false;
@@ -1256,11 +1318,15 @@ export function parseMacroContext(macroText, cursorOffset) {
// Cursor is before the prefix - still in flags area conceptually
isTypingVariableName = false;
} else if (cursorOffset <= variableNameEnd) {
// Cursor is in the variable name
// Cursor is in the variable name area (including at the end)
isTypingVariableName = true;
} else if (!variableOperator && !hasInvalidTrailingChars) {
} else if (variableName.length > 0 && !variableOperator && !hasInvalidTrailingChars) {
// Cursor is after variable name but no operator yet (and no invalid chars)
// This includes partial operator prefixes like '+', '-', '|', '?'
// This includes partial operator prefixes like '+', '-', '|', '?', '>', '<'
isTypingOperator = true;
} else if (variableName.length > 0 && variableOperator && isShortOperatorPrefix(variableOperator) && cursorOffset <= variableOperatorEnd) {
// Short operator that could be prefix of longer one (e.g., > could become >=)
// But ONLY if cursor is still in the operator area, not past it into value
isTypingOperator = true;
} else if (operatorNeedsValue) {
// Operator that requires value - cursor is in value area
@@ -1292,7 +1358,9 @@ export function parseMacroContext(macroText, cursorOffset) {
isVariableShorthand,
variablePrefix,
variableName,
variableNameEnd,
variableOperator,
variableOperatorEnd,
variableValue,
isTypingVariableName,
isTypingOperator,
@@ -574,6 +574,22 @@ class MacroCstWalker {
operation = 'notEquals';
hasValueExpr = true;
break;
case '>':
operation = 'greaterThan';
hasValueExpr = true;
break;
case '>=':
operation = 'greaterThanOrEqual';
hasValueExpr = true;
break;
case '<':
operation = 'lessThan';
hasValueExpr = true;
break;
case '<=':
operation = 'lessThanOrEqual';
hasValueExpr = true;
break;
default:
logMacroInternalError({ message: `Lexer found macro operator that is not implemented for variable shorthand expressions in macro node '${macroNode.name}'.` });
break;
@@ -714,6 +730,50 @@ class MacroCstWalker {
return currentValue !== compareValue ? 'true' : 'false';
}
case 'greaterThan': {
// Numeric greater than comparison
const currentNum = Number(vars.get(varName));
const compareNum = Number(lazyValue());
if (isNaN(currentNum) || isNaN(compareNum)) {
logMacroRuntimeWarning({ message: `Variable shorthand ">" operator requires numeric values. Got: "${vars.get(varName)}" > "${lazyValue()}"` });
return 'false';
}
return currentNum > compareNum ? 'true' : 'false';
}
case 'greaterThanOrEqual': {
// Numeric greater than or equal comparison
const currentNum = Number(vars.get(varName));
const compareNum = Number(lazyValue());
if (isNaN(currentNum) || isNaN(compareNum)) {
logMacroRuntimeWarning({ message: `Variable shorthand ">=" operator requires numeric values. Got: "${vars.get(varName)}" >= "${lazyValue()}"` });
return 'false';
}
return currentNum >= compareNum ? 'true' : 'false';
}
case 'lessThan': {
// Numeric less than comparison
const currentNum = Number(vars.get(varName));
const compareNum = Number(lazyValue());
if (isNaN(currentNum) || isNaN(compareNum)) {
logMacroRuntimeWarning({ message: `Variable shorthand "<" operator requires numeric values. Got: "${vars.get(varName)}" < "${lazyValue()}"` });
return 'false';
}
return currentNum < compareNum ? 'true' : 'false';
}
case 'lessThanOrEqual': {
// Numeric less than or equal comparison
const currentNum = Number(vars.get(varName));
const compareNum = Number(lazyValue());
if (isNaN(currentNum) || isNaN(compareNum)) {
logMacroRuntimeWarning({ message: `Variable shorthand "<=" operator requires numeric values. Got: "${vars.get(varName)}" <= "${lazyValue()}"` });
return 'false';
}
return currentNum <= compareNum ? 'true' : 'false';
}
default:
logMacroRuntimeWarning({ message: `Unknown variable shorthand operation: "${operation}"` });
return '';
@@ -132,6 +132,14 @@ const Tokens = Object.freeze({
DoubleEquals: createToken({ name: 'Var.DoubleEquals', pattern: /==/ }),
/** Not equals comparison operator (`!=`) - compares variable to value, returns inverted result */
NotEquals: createToken({ name: 'Var.NotEquals', pattern: /!=/ }),
/** Greater than or equal comparison operator (`>=`) - must come before GreaterThan */
GreaterThanOrEqual: createToken({ name: 'Var.GreaterThanOrEqual', pattern: />=/ }),
/** Greater than comparison operator (`>`) */
GreaterThan: createToken({ name: 'Var.GreaterThan', pattern: />/ }),
/** Less than or equal comparison operator (`<=`) - must come before LessThan */
LessThanOrEqual: createToken({ name: 'Var.LessThanOrEqual', pattern: /<=/ }),
/** Less than comparison operator (`<`) */
LessThan: createToken({ name: 'Var.LessThan', pattern: /</ }),
/** Add/append operator (`+=`) - must come before Equals to avoid conflict */
PlusEquals: createToken({ name: 'Var.PlusEquals', pattern: /\+=/ }),
/** Set operator (`=`) */
@@ -258,6 +266,10 @@ const Def = {
enter(Tokens.Var.Operators.MinusEquals, modes.var_value, { andExits: modes.var_after_identifier }),
enter(Tokens.Var.Operators.DoubleEquals, modes.var_value, { andExits: modes.var_after_identifier }),
enter(Tokens.Var.Operators.NotEquals, modes.var_value, { andExits: modes.var_after_identifier }),
enter(Tokens.Var.Operators.GreaterThanOrEqual, modes.var_value, { andExits: modes.var_after_identifier }),
enter(Tokens.Var.Operators.GreaterThan, modes.var_value, { andExits: modes.var_after_identifier }),
enter(Tokens.Var.Operators.LessThanOrEqual, modes.var_value, { andExits: modes.var_after_identifier }),
enter(Tokens.Var.Operators.LessThan, modes.var_value, { andExits: modes.var_after_identifier }),
enter(Tokens.Var.Operators.PlusEquals, modes.var_value, { andExits: modes.var_after_identifier }),
enter(Tokens.Var.Operators.Equals, modes.var_value, { andExits: modes.var_after_identifier }),
// If we see the end, exit
+16 -50
View File
@@ -92,65 +92,31 @@ class MacroParser extends CstParser {
$.OPTION2(() => $.SUBRULE($.variableOperator));
});
// Variable operator: ++, --, = value, += value, -= value, ||, ??, ||=, ??=, ==, !=
// Variable operator: ++, --, = value, += value, -= value, ||, ??, ||=, ??=, ==, !=, >, >=, <, <=
$.variableOperator = $.RULE('variableOperator', () => {
$.OR4([
{ ALT: () => $.CONSUME(Tokens.Var.Operators.Increment, { LABEL: 'Var.operator' }) },
{ ALT: () => $.CONSUME(Tokens.Var.Operators.Decrement, { LABEL: 'Var.operator' }) },
{
ALT: () => {
$.CONSUME(Tokens.Var.Operators.NullishCoalescingEquals, { LABEL: 'Var.operator' });
$.OR5([
{ ALT: () => $.CONSUME(Tokens.Var.Operators.NullishCoalescingEquals, { LABEL: 'Var.operator' }) },
{ ALT: () => $.CONSUME(Tokens.Var.Operators.NullishCoalescing, { LABEL: 'Var.operator' }) },
{ ALT: () => $.CONSUME(Tokens.Var.Operators.LogicalOrEquals, { LABEL: 'Var.operator' }) },
{ ALT: () => $.CONSUME(Tokens.Var.Operators.LogicalOr, { LABEL: 'Var.operator' }) },
{ ALT: () => $.CONSUME(Tokens.Var.Operators.MinusEquals, { LABEL: 'Var.operator' }) },
{ ALT: () => $.CONSUME(Tokens.Var.Operators.DoubleEquals, { LABEL: 'Var.operator' }) },
{ ALT: () => $.CONSUME(Tokens.Var.Operators.NotEquals, { LABEL: 'Var.operator' }) },
{ ALT: () => $.CONSUME(Tokens.Var.Operators.GreaterThanOrEqual, { LABEL: 'Var.operator' }) },
{ ALT: () => $.CONSUME(Tokens.Var.Operators.GreaterThan, { LABEL: 'Var.operator' }) },
{ ALT: () => $.CONSUME(Tokens.Var.Operators.LessThanOrEqual, { LABEL: 'Var.operator' }) },
{ ALT: () => $.CONSUME(Tokens.Var.Operators.LessThan, { LABEL: 'Var.operator' }) },
{ ALT: () => $.CONSUME(Tokens.Var.Operators.PlusEquals, { LABEL: 'Var.operator' }) },
{ ALT: () => $.CONSUME(Tokens.Var.Operators.Equals, { LABEL: 'Var.operator' }) },
]);
$.SUBRULE($.variableValue, { LABEL: 'Var.value' });
},
},
{
ALT: () => {
$.CONSUME(Tokens.Var.Operators.NullishCoalescing, { LABEL: 'Var.operator' });
$.SUBRULE2($.variableValue, { LABEL: 'Var.value' });
},
},
{
ALT: () => {
$.CONSUME(Tokens.Var.Operators.LogicalOrEquals, { LABEL: 'Var.operator' });
$.SUBRULE3($.variableValue, { LABEL: 'Var.value' });
},
},
{
ALT: () => {
$.CONSUME(Tokens.Var.Operators.LogicalOr, { LABEL: 'Var.operator' });
$.SUBRULE4($.variableValue, { LABEL: 'Var.value' });
},
},
{
ALT: () => {
$.CONSUME(Tokens.Var.Operators.MinusEquals, { LABEL: 'Var.operator' });
$.SUBRULE5($.variableValue, { LABEL: 'Var.value' });
},
},
{
ALT: () => {
$.CONSUME(Tokens.Var.Operators.DoubleEquals, { LABEL: 'Var.operator' });
$.SUBRULE6($.variableValue, { LABEL: 'Var.value' });
},
},
{
ALT: () => {
$.CONSUME(Tokens.Var.Operators.NotEquals, { LABEL: 'Var.operator' });
$.SUBRULE7($.variableValue, { LABEL: 'Var.value' });
},
},
{
ALT: () => {
$.CONSUME(Tokens.Var.Operators.PlusEquals, { LABEL: 'Var.operator' });
$.SUBRULE8($.variableValue, { LABEL: 'Var.value' });
},
},
{
ALT: () => {
$.CONSUME(Tokens.Var.Operators.Equals, { LABEL: 'Var.operator' });
$.SUBRULE9($.variableValue, { LABEL: 'Var.value' });
},
},
]);
});
@@ -658,17 +658,13 @@ export class SlashCommandParser {
resultStart = macro.start + 2 + prefixIndex + 1; // +1 to skip the prefix
}
} else if (context.isTypingOperator) {
// Typing operator: identifier = partial operator text (if any), start = after variable name
// Using partial operator as identifier ensures cursor is within name range for filtering
resultIdentifier = context.partialOperator || '';
if (prefixIndex >= 0) {
// Start after prefix + variable name length
resultStart = macro.start + 2 + prefixIndex + 1 + context.variableName.length;
// If no partial operator (just whitespace after var name), set start to cursor
// This ensures cursor is in the name range for filtering
if (!context.partialOperator) {
resultStart = index;
}
// Typing operator: identifier = partial operator or current operator, start = after variable name
resultIdentifier = context.partialOperator || context.variableOperator || '';
// Use actual variableNameEnd position from parsing (accounts for whitespace)
resultStart = macro.start + 2 + context.variableNameEnd;
// Skip whitespace between variable name and operator
while (resultStart < index && /\s/.test(text[resultStart])) {
resultStart++;
}
} else if (context.isOperatorComplete) {
// Operator complete (++ or --) - show context but no value input needed
@@ -677,23 +673,20 @@ export class SlashCommandParser {
} else if (context.hasInvalidTrailingChars) {
// Invalid chars after variable name: show the invalid chars for warning
resultIdentifier = context.invalidTrailingChars || '';
if (prefixIndex >= 0) {
resultStart = macro.start + 2 + prefixIndex + 1 + context.variableName.length;
}
// Use actual variableNameEnd position from parsing
resultStart = macro.start + 2 + context.variableNameEnd;
} else if (context.isTypingValue) {
// Typing value: identifier = value being typed, start = after operator
resultIdentifier = context.variableValue;
if (prefixIndex >= 0) {
const operatorLen = context.variableOperator?.length ?? 0;
resultStart = macro.start + 2 + prefixIndex + 1 + context.variableName.length + operatorLen;
// Skip any whitespace between operator and value
while (resultStart < index && /\s/.test(text[resultStart])) {
resultStart++;
}
makeNoMatchText = () => `Type any value you want to ${context.variableOperator == '+=' ? `add to the variable '${context.variableName}'` : `set the variable '${context.variableName}' to`}.`;
makeNoOptionsText = () => 'Enter a variable value';
// Use actual operatorEnd position from parsing (accounts for whitespace)
resultStart = macro.start + 2 + context.variableOperatorEnd;
// Skip any whitespace between operator and value
while (resultStart < index && /\s/.test(text[resultStart])) {
resultStart++;
}
makeNoMatchText = () => `Type any value you want to ${context.variableOperator == '+=' ? `add to the variable '${context.variableName}'` : `set the variable '${context.variableName}' to`}.`;
makeNoOptionsText = () => 'Enter a variable value';
} else {
// Fallback: use variable name
resultIdentifier = context.variableName;
@@ -952,16 +945,20 @@ export class SlashCommandParser {
prefixOption.valueProvider = () => ''; // Already typed, don't re-insert
prefixOption.makeSelectable = false;
prefixOption.sortPriority = 1; // Show at top
prefixOption.matchProvider = () => true; // Always show regardless of filtering
options.push(prefixOption);
}
// If typing the variable name, suggest existing variables
if (context.isTypingVariableName) {
// Get existing variable names from the appropriate scope
// Filter to only include names that are valid for shorthand syntax
const existingVariables = this.#getVariableNames(scope)
.filter(name => isValidVariableShorthandName(name));
// Get existing variable names from the appropriate scope
// Filter to only include names that are valid for shorthand syntax
const existingVariables = this.#getVariableNames(scope)
.filter(name => isValidVariableShorthandName(name));
// Check if the typed variable name exactly matches an existing variable
const variableNameMatchesExisting = context.variableName.length > 0 && existingVariables.includes(context.variableName);
if (context.isTypingVariableName) {
// Add existing variables that match the typed name
for (const varName of existingVariables) {
const option = new VariableNameAutoCompleteOption(varName, scope, false);
@@ -995,6 +992,20 @@ export class SlashCommandParser {
}
options.push(newVarOption);
}
// If the typed variable name exactly matches an existing variable, also show operators
// This allows users to see available operators without having to type a space first
if (variableNameMatchesExisting) {
for (const [, operatorDef] of VariableOperatorDefinitions) {
const opOption = new VariableOperatorAutoCompleteOption(operatorDef);
opOption.sortPriority = 6; // Lower priority than variable suggestions
opOption.matchProvider = () => true; // Always show
// IMPORTANT: Operators should INSERT after variable name, not replace it
// Use replacementStartOffset to shift insertion point past the variable name
opOption.replacementStartOffset = context.variableName.length;
options.push(opOption);
}
}
}
// If there are invalid trailing characters after the variable name, show a warning
@@ -1026,14 +1037,23 @@ export class SlashCommandParser {
options.push(varNameOption);
// Then show available operators, filtered by partial prefix if any
// Also filter by current complete operator to show longer variants (e.g., > shows >=)
const partialOp = context.partialOperator || '';
const currentOp = context.variableOperator || '';
const filterPrefix = partialOp || currentOp;
for (const [, operatorDef] of VariableOperatorDefinitions) {
// Filter by partial operator prefix if user is typing one
if (partialOp && !operatorDef.symbol.startsWith(partialOp)) {
// Filter by operator prefix if user is typing one
// This allows typing ">" to show both ">" and ">="
if (filterPrefix && !operatorDef.symbol.startsWith(filterPrefix)) {
continue;
}
const opOption = new VariableOperatorAutoCompleteOption(operatorDef);
opOption.sortPriority = 5;
// Exact match gets higher priority
opOption.sortPriority = operatorDef.symbol === currentOp ? 4 : 5;
// Already-typed operator is non-selectable
if (operatorDef.symbol === currentOp) {
opOption.valueProvider = () => '';
}
// Always match operators when showing operator suggestions
opOption.matchProvider = () => true;
options.push(opOption);
@@ -1041,21 +1061,23 @@ export class SlashCommandParser {
}
// If typing value (after = or +=), no autocomplete needed - freeform text
// But we can show the current context for reference
if (context.isTypingValue) {
// Show the current variable name as context
// But we show the current context for reference (greyed out, non-selectable)
if (context.isTypingValue && !context.isTypingOperator) {
// Show the current variable name as context (non-selectable)
const varNameOption = new VariableNameAutoCompleteOption(context.variableName, scope, false);
varNameOption.valueProvider = () => ''; // Context only
varNameOption.makeSelectable = false;
varNameOption.sortPriority = 2;
varNameOption.matchProvider = () => true; // Always show
options.push(varNameOption);
// Show the operator that was used
// Show the operator that was used (non-selectable)
if (context.variableOperator) {
const opDef = VariableOperatorDefinitions.get(context.variableOperator);
if (opDef) {
const opOption = new VariableOperatorAutoCompleteOption(opDef);
opOption.valueProvider = () => ''; // Already typed
opOption.makeSelectable = false;
opOption.sortPriority = 3;
opOption.matchProvider = () => true; // Always show
options.push(opOption);
@@ -1063,21 +1085,23 @@ export class SlashCommandParser {
}
}
// If operator is complete (++ or --), show context without value input
if (context.isOperatorComplete) {
// Show the current variable name as context
// If operator is complete (++ or --), show context without value input (non-selectable)
if (context.isOperatorComplete && !context.isTypingOperator) {
// Show the current variable name as context (non-selectable)
const varNameOption = new VariableNameAutoCompleteOption(context.variableName, scope, false);
varNameOption.valueProvider = () => ''; // Context only
varNameOption.makeSelectable = false;
varNameOption.sortPriority = 2;
varNameOption.matchProvider = () => true; // Always show
options.push(varNameOption);
// Show the operator that was used
// Show the operator that was used (non-selectable)
if (context.variableOperator) {
const opDef = VariableOperatorDefinitions.get(context.variableOperator);
if (opDef) {
const opOption = new VariableOperatorAutoCompleteOption(opDef);
opOption.valueProvider = () => ''; // Already typed
opOption.makeSelectable = false;
opOption.sortPriority = 3;
opOption.matchProvider = () => true; // Always show
options.push(opOption);
+96
View File
@@ -2699,6 +2699,102 @@ test.describe('MacroEngine', () => {
expect(output).toBe('true');
});
// {{.myvar > value}} - greater than comparison (numeric)
test('should return true when variable is greater than value with >', async ({ page }) => {
const output = await evaluateWithEngineAndVariables(page, '{{.myvar > 5}}', { local: { myvar: '10' } });
expect(output).toBe('true');
});
test('should return false when variable is not greater than value with >', async ({ page }) => {
const output = await evaluateWithEngineAndVariables(page, '{{.myvar > 10}}', { local: { myvar: '5' } });
expect(output).toBe('false');
});
test('should return false when variable equals value with >', async ({ page }) => {
const output = await evaluateWithEngineAndVariables(page, '{{.myvar > 10}}', { local: { myvar: '10' } });
expect(output).toBe('false');
});
test('should return false for non-numeric values with >', async ({ page }) => {
const output = await evaluateWithEngineAndVariables(page, '{{.myvar > 5}}', { local: { myvar: 'abc' } });
expect(output).toBe('false');
});
// {{.myvar >= value}} - greater than or equal comparison (numeric)
test('should return true when variable is greater than value with >=', async ({ page }) => {
const output = await evaluateWithEngineAndVariables(page, '{{.myvar >= 5}}', { local: { myvar: '10' } });
expect(output).toBe('true');
});
test('should return true when variable equals value with >=', async ({ page }) => {
const output = await evaluateWithEngineAndVariables(page, '{{.myvar >= 10}}', { local: { myvar: '10' } });
expect(output).toBe('true');
});
test('should return false when variable is less than value with >=', async ({ page }) => {
const output = await evaluateWithEngineAndVariables(page, '{{.myvar >= 10}}', { local: { myvar: '5' } });
expect(output).toBe('false');
});
// {{.myvar < value}} - less than comparison (numeric)
test('should return true when variable is less than value with <', async ({ page }) => {
const output = await evaluateWithEngineAndVariables(page, '{{.myvar < 10}}', { local: { myvar: '5' } });
expect(output).toBe('true');
});
test('should return false when variable is not less than value with <', async ({ page }) => {
const output = await evaluateWithEngineAndVariables(page, '{{.myvar < 5}}', { local: { myvar: '10' } });
expect(output).toBe('false');
});
test('should return false when variable equals value with <', async ({ page }) => {
const output = await evaluateWithEngineAndVariables(page, '{{.myvar < 10}}', { local: { myvar: '10' } });
expect(output).toBe('false');
});
test('should return false for non-numeric values with <', async ({ page }) => {
const output = await evaluateWithEngineAndVariables(page, '{{.myvar < 5}}', { local: { myvar: 'abc' } });
expect(output).toBe('false');
});
// {{.myvar <= value}} - less than or equal comparison (numeric)
test('should return true when variable is less than value with <=', async ({ page }) => {
const output = await evaluateWithEngineAndVariables(page, '{{.myvar <= 10}}', { local: { myvar: '5' } });
expect(output).toBe('true');
});
test('should return true when variable equals value with <=', async ({ page }) => {
const output = await evaluateWithEngineAndVariables(page, '{{.myvar <= 10}}', { local: { myvar: '10' } });
expect(output).toBe('true');
});
test('should return false when variable is greater than value with <=', async ({ page }) => {
const output = await evaluateWithEngineAndVariables(page, '{{.myvar <= 5}}', { local: { myvar: '10' } });
expect(output).toBe('false');
});
// Negative numbers with comparison operators
test('should handle negative numbers with > operator', async ({ page }) => {
const output = await evaluateWithEngineAndVariables(page, '{{.myvar > -5}}', { local: { myvar: '0' } });
expect(output).toBe('true');
});
test('should handle negative numbers with < operator', async ({ page }) => {
const output = await evaluateWithEngineAndVariables(page, '{{.myvar < 0}}', { local: { myvar: '-5' } });
expect(output).toBe('true');
});
// Decimal numbers with comparison operators
test('should handle decimal numbers with >= operator', async ({ page }) => {
const output = await evaluateWithEngineAndVariables(page, '{{.myvar >= 3.14}}', { local: { myvar: '3.14' } });
expect(output).toBe('true');
});
test('should handle decimal numbers with <= operator', async ({ page }) => {
const output = await evaluateWithEngineAndVariables(page, '{{.myvar <= 2.5}}', { local: { myvar: '2.49' } });
expect(output).toBe('true');
});
// Global variable versions of new operators
test('should use || with global variable', async ({ page }) => {
const output = await evaluateWithEngineAndVariables(page, '{{$myvar || globaldefault}}', { global: { myvar: '' } });