diff --git a/public/scripts/autocomplete/EnhancedMacroAutoCompleteOption.js b/public/scripts/autocomplete/EnhancedMacroAutoCompleteOption.js index 3b4720bf9..b0ded14f3 100644 --- a/public/scripts/autocomplete/EnhancedMacroAutoCompleteOption.js +++ b/public/scripts/autocomplete/EnhancedMacroAutoCompleteOption.js @@ -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} @@ -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, diff --git a/public/scripts/macros/engine/MacroCstWalker.js b/public/scripts/macros/engine/MacroCstWalker.js index 7ae5b3ba3..9abafba79 100644 --- a/public/scripts/macros/engine/MacroCstWalker.js +++ b/public/scripts/macros/engine/MacroCstWalker.js @@ -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 ''; diff --git a/public/scripts/macros/engine/MacroLexer.js b/public/scripts/macros/engine/MacroLexer.js index 2eff3bd84..333f120f8 100644 --- a/public/scripts/macros/engine/MacroLexer.js +++ b/public/scripts/macros/engine/MacroLexer.js @@ -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: / $.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' }); - }, - }, ]); }); diff --git a/public/scripts/slash-commands/SlashCommandParser.js b/public/scripts/slash-commands/SlashCommandParser.js index 3c06afb91..39cc9f5d6 100644 --- a/public/scripts/slash-commands/SlashCommandParser.js +++ b/public/scripts/slash-commands/SlashCommandParser.js @@ -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); diff --git a/tests/frontend/MacroEngine.e2e.js b/tests/frontend/MacroEngine.e2e.js index 4c3fe78f2..27668ca22 100644 --- a/tests/frontend/MacroEngine.e2e.js +++ b/tests/frontend/MacroEngine.e2e.js @@ -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: '' } });