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:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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: '' } });
|
||||
|
||||
Reference in New Issue
Block a user