Macros 2.0 - Improve Autocomplete edge cases on completing macros (#5093) * fix(autocomplete): handle nested macros in :: separator parsing Track nesting depth when parsing macro parts to avoid treating :: inside nested macros as separators. Increment depth on {{ and decrement on }}, only recognizing :: as a separator when nestedDepth is 0. * chore(autocomplete): add missing properties for non-variable macros Add `variableNameEnd` and `variableOperatorEnd` fields to track end positions of variable components in macro parsing context. * fix(autocomplete): stop macro parsing at newlines before first :: separator Prevent newlines before the first :: separator from being included in macro identifier/space-arg parsing. Track separator state and break early when encountering newlines at depth 0 before any :: is seen. Only include the last part if cursor is within its bounds to avoid extending macro context across line breaks. * fix(autocomplete): handle cursor position after closing braces in macro detection Skip closing }} when searching backwards if cursor is immediately after them, treating them as the target macro's closing braces rather than nested braces. Skip forward search entirely when cursor is after }}, using cursor position as closePos directly. * fix(autocomplete): show macro details when typing closing brace and add no-match indicator Track `isTypingClosingBrace` state when cursor is after a single `}` on standalone macros (no separators/args). Strip trailing `}` from identifier in this case. Show matching macro details when typing closing brace, same as when typing args. Add "no match" error option when typing args/closing brace but identifier doesn't match any macro. Extract `AnyMacroAutoCompleteOption` typedef for cleaner type annotations * fix(autocomplete): show variable context when typing closing brace on shorthand macros Detect `isTypingClosingBrace` state for variable shorthands (e.g., `{{.Lila}`, `{{.Lila+=4}`). Parse value before brace detection to check for trailing `}`. Show non-selectable context options (variable name, operator, value) when typing closing brace instead of operator/value suggestions. Strip trailing `}` from value when detected. Set `resultStart` to cursor position when typing closing brace to avoid replacement * fix(autocomplete): stop flag parsing at newlines and show context when typing closing brace on variable shorthands Skip newlines (not just all whitespace) between flags to prevent extending macro context across line breaks. Show variable context (name, operator, value) when typing closing brace on shorthand macros by excluding `isTypingClosingBrace` from the value-typing check. * Fix highlight of value autocomplete option * Rename permanentMatch to forceFullNameMatch --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>

06b77ec94c0b9107bbe4e7e049976083b711c73b

Wolfsblvt <wolfsblvt@gmail.com>

Signed
4 files changed, +196 -17Showing whitespace changes
public/scripts/autocomplete/AutoComplete.js+4 -0
@@ -155,6 +155,10 @@ export class AutoComplete {
155155 */
156156 updateName(item) {
157157 const chars = Array.from(item.dom.querySelector('.name').children);
158+ if (item.forceFullNameMatch) {
159+ chars.forEach(c => c.classList.toggle('matched', true));
160+ return;
161+ }
158162 switch (this.matchType) {
159163 case 'strict': {
160164 chars.forEach((it, idx) => {
public/scripts/autocomplete/AutoCompleteOption.js+1 -0
@@ -13,6 +13,7 @@ export class AutoCompleteOption {
1313 /** @type {(input:string)=>boolean} */ matchProvider;
1414 /** @type {(input:string)=>string} */ valueProvider;
1515 /** @type {boolean} */ makeSelectable = false;
16+ /** @type {boolean} */ forceFullNameMatch = false;
1617
1718 /**
1819 * Offset to adjust the replacement start position.
public/scripts/autocomplete/EnhancedMacroAutoCompleteOption.js+91 -8
@@ -31,6 +31,7 @@ import { onboardingExperimentalMacroEngine } from '../macros/engine/MacroDiagnos
3131 * @property {string[]} args - Array of arguments typed so far.
3232 * @property {number} currentArgIndex - Index of the argument being typed (-1 if on identifier).
3333 * @property {boolean} isTypingSeparator - Whether cursor is on a partial separator (single ':').
34+ * @property {boolean} isTypingClosingBrace - Whether cursor is typing the first closing brace on a standalone macro.
3435 * @property {boolean} hasSpaceAfterIdentifier - Whether there's a space after the identifier (for space-separated args).
3536 * @property {boolean} hasSpaceArgContent - Whether there's actual content after the space (not just whitespace).
3637 * @property {number} separatorCount - Number of '::' separators found.
@@ -1033,6 +1034,7 @@ export class VariableValueContextAutoCompleteOption extends AutoCompleteOption {
10331034 super('value', '📝');
10341035 this.#operatorDef = operatorDef;
10351036 this.#currentValue = currentValue;
1037+ this.forceFullNameMatch = true;
10361038 }
10371039
10381040 /** @returns {{ symbol: string, name: string, description: string, needsValue: boolean }} */
@@ -1236,8 +1238,8 @@ export class MacroClosingTagAutoCompleteOption extends AutoCompleteOption {
12361238export function parseMacroContext(macroText, cursorOffset) {
12371239 let i = 0;
12381240
1239- // Skip leading whitespace
1241+ // Skip leading whitespace (but NOT newlines - those stop macro parsing for autocomplete)
12401242 while (i < macroText.length && /[ \st]/.test(macroText[i])) {
12411243 i++;
12421244 }
12431245
@@ -1257,8 +1259,8 @@ export function parseMacroContext(macroText, cursorOffset) {
12571259 flags.push(char);
12581260 i++;
12591261 flagEndPositions.push(i); // Position right after this flag
12601262 // Skip whitespace between flags (but NOT newlines - those stop macro parsing for autocomplete)
12611263 while (i < macroText.length && /[ \st]/.test(macroText[i])) {
12621264 i++;
12631265 }
12641266 } else {
@@ -1387,15 +1389,49 @@ export function parseMacroContext(macroText, cursorOffset) {
13871389 const operatorNeedsValue = operatorDef?.needsValue ?? false;
13881390
13891391 // If operator requires a value, parse the value
1392+ // Do this BEFORE isTypingClosingBrace detection so we can check for } in value area
1393+ // let valueStartPos = i;
13901394 if (operatorNeedsValue) {
13911395 // Skip whitespace after operator
13921396 while (i < macroText.length && /\s/.test(macroText[i])) {
13931397 i++;
13941398 }
1399+ // valueStartPos = i;
13951400 variableValue = macroText.slice(i).trimEnd();
13961401 }
13971402
1403+ // Detect if typing first closing brace on a variable shorthand
1404+ // This happens when operatorText is just "}" or when cursor is beyond content (after }})
1405+ let isTypingClosingBrace = false;
1406+ if (operatorText.startsWith('}') && !variableOperator) {
1407+ // Typing first } on a standalone variable shorthand like {{.Lila}
1408+ isTypingClosingBrace = true;
1409+ } else if (cursorOffset > macroText.length && !variableOperator) {
1410+ // Cursor is after }} on a standalone variable shorthand like {{.Lila}}|
1411+ isTypingClosingBrace = true;
1412+ } else if (cursorOffset > macroText.length && variableOperator) {
1413+ // Cursor is after }} on any operator shorthand like {{.Lila++}}| or {{.Lila+=4}}|
1414+ isTypingClosingBrace = true;
1415+ } else if (cursorOffset >= macroText.length && variableOperator && !operatorNeedsValue) {
1416+ // Cursor at end of complete operator (++ or --) like {{.Lila++ or {{.Lila++ (with trailing space)
1417+ isTypingClosingBrace = true;
1418+ } else if (cursorOffset >= macroText.length && !variableOperator && variableName.length > 0) {
1419+ // Cursor at end of standalone variable (with or without trailing whitespace) like {{.Lila or {{ .Lila
1420+ isTypingClosingBrace = true;
1421+ } else if (operatorNeedsValue && variableValue.length > 0 && variableValue.endsWith('}')) {
1422+ // Typing first } after a value like {{.Lila+=4}
1423+ isTypingClosingBrace = true;
1424+ // Strip the } from the value
1425+ variableValue = variableValue.slice(0, -1);
1426+ } else if (operatorNeedsValue && cursorOffset >= macroText.length && variableValue.length > 0) {
1427+ // Cursor at end after typing a value (including trailing whitespace) like {{.Lila+=4
1428+ // This means the shorthand is "complete" and ready to close
1429+ isTypingClosingBrace = true;
1430+ }
1431+
13981432 // Determine cursor position context for autocomplete
1433+ // Note: isTypingClosingBrace takes precedence - if we're typing a closing brace,
1434+ // we don't want to show operator suggestions, just the current state
13991435 const prefixEnd = (macroText.indexOf(variablePrefix) ?? 0) + 1;
14001436 if (cursorOffset < prefixEnd) {
14011437 // Cursor is before the prefix - still in flags area conceptually
@@ -1403,9 +1439,10 @@ export function parseMacroContext(macroText, cursorOffset) {
14031439 } else if (cursorOffset <= variableNameEnd) {
14041440 // Cursor is in the variable name area (including at the end)
14051441 isTypingVariableName = true;
14061442 } else if (variableName.length > 0 && !variableOperator && !hasInvalidTrailingChars && !isTypingClosingBrace) {
14071443 // Cursor is after variable name but no operator yet (and no invalid chars)
14081444 // This includes partial operator prefixes like '+', '-', '|', '?', '>', '<'
1445+ // But NOT when typing a closing brace - that takes precedence
14091446 isTypingOperator = true;
14101447 } else if (variableName.length > 0 && variableOperator && isShortOperatorPrefix(variableOperator) && cursorOffset <= variableOperatorEnd) {
14111448 // Short operator that could be prefix of longer one (e.g., > could become >=)
@@ -1434,6 +1471,7 @@ export function parseMacroContext(macroText, cursorOffset) {
14341471 args: [],
14351472 currentArgIndex: -1,
14361473 isTypingSeparator: false,
1474+ isTypingClosingBrace,
14371475 hasSpaceAfterIdentifier: false,
14381476 hasSpaceArgContent: false,
14391477 separatorCount: 0,
@@ -1465,20 +1503,55 @@ export function parseMacroContext(macroText, cursorOffset) {
14651503 let partStart = i;
14661504 let j = 0;
14671505
1506+ // Track nesting depth to skip :: inside nested macros
1507+ let nestedDepth = 0;
1508+ // Track if we've seen a :: separator - newlines before first :: should stop parsing
1509+ let hasSeenSeparator = false;
1510+ // Track if we broke early (e.g., at a newline)
1511+ let brokeEarly = false;
14681512 while (j < remainingText.length) {
1469- if (remainingText[j] === ':' && remainingText[j + 1] === ':') {
1513+ // Before the first :: separator, newlines should stop parsing
1514+ // This prevents text on the next line from being considered part of the identifier/space-arg
1515+ if (!hasSeenSeparator && nestedDepth === 0 && (remainingText[j] === '\n' || remainingText[j] === '\r')) {
1516+ // Stop parsing here - don't include the newline or anything after
1517+ brokeEarly = true;
1518+ break;
1519+ }
1520+ // Track nested macro braces
1521+ if (remainingText[j] === '{' && remainingText[j + 1] === '{') {
1522+ nestedDepth++;
1523+ currentPart += '{{';
1524+ j += 2;
1525+ continue;
1526+ }
1527+ if (remainingText[j] === '}' && remainingText[j + 1] === '}') {
1528+ nestedDepth = Math.max(0, nestedDepth - 1);
1529+ currentPart += '}}';
1530+ j += 2;
1531+ continue;
1532+ }
1533+ // Only count :: as separator when not inside nested macros
1534+ if (nestedDepth === 0 && remainingText[j] === ':' && remainingText[j + 1] === ':') {
14701535 parts.push({ text: currentPart, start: partStart, end: i + j });
14711536 separatorPositions.push({ start: i + j, end: i + j + 2 });
14721537 currentPart = '';
14731538 j += 2;
14741539 partStart = i + j;
1540+ hasSeenSeparator = true;
14751541 } else {
14761542 currentPart += remainingText[j];
14771543 j++;
14781544 }
14791545 }
14801546 // Push the last part - use correct end position if we broke early.
1481- parts.push({ text: currentPart, start: partStart, end: macroText.length });
1547+ // If we broke early (at a newline) AND cursor is past that point, don't push -
1548+ // this filters out text on the next line from being considered part of this macro.
1549+ // But if we didn't break early (cursor at end of closed macro), always push.
1550+ const lastPartEnd = brokeEarly ? i + j : macroText.length;
1551+ const shouldPushLastPart = !brokeEarly || cursorOffset <= lastPartEnd;
1552+ if (shouldPushLastPart) {
1553+ parts.push({ text: currentPart, start: partStart, end: lastPartEnd });
1554+ }
14821555
14831556 // Determine if cursor is in the flags area (at or before identifier starts)
14841557 const identifierStartPos = parts[0]?.start ?? i;
@@ -1553,7 +1626,14 @@ export function parseMacroContext(macroText, cursorOffset) {
15531626 }
15541627
15551628 // Clean identifier: strip trailing colons (for partial :: typing)
1629+ // Also strip trailing single } (for partial }} typing) - but only if no separators/args
15561630 let cleanIdentifier = identifierOnly.replace(/:+$/, '');
1631+ let isTypingClosingBrace = false;
1632+ if (separatorPositions.length === 0 && !hasSpaceAfterIdentifier && cleanIdentifier.endsWith('}')) {
1633+ // Typing first closing brace on a standalone macro like {{char}
1634+ cleanIdentifier = cleanIdentifier.slice(0, -1);
1635+ isTypingClosingBrace = true;
1636+ }
15571637
15581638 // Build args array - include space-separated arg if present
15591639 // Trim args like the macro engine does
@@ -1574,6 +1654,7 @@ export function parseMacroContext(macroText, cursorOffset) {
15741654 args,
15751655 currentArgIndex,
15761656 isTypingSeparator,
1657+ isTypingClosingBrace,
15771658 hasSpaceAfterIdentifier,
15781659 hasSpaceArgContent: spaceArgText.length > 0,
15791660 separatorCount: separatorPositions.length,
@@ -1581,7 +1662,9 @@ export function parseMacroContext(macroText, cursorOffset) {
15811662 isVariableShorthand: false,
15821663 variablePrefix: null,
15831664 variableName: '',
1665+ variableNameEnd: null,
15841666 variableOperator: null,
1667+ variableOperatorEnd: null,
15851668 variableValue: '',
15861669 isTypingVariableName: false,
15871670 isTypingOperator: false,
public/scripts/autocomplete/MacroAutoCompleteHelper.js+100 -9
@@ -57,6 +57,8 @@ import { extension_settings } from '../extensions.js';
5757 * @property {UnclosedScope[]|null} [unclosedScopes=null] - Pre-computed unclosed scopes
5858 */
5959
60+/** @typedef {(EnhancedMacroAutoCompleteOption|MacroFlagAutoCompleteOption|MacroClosingTagAutoCompleteOption|VariableShorthandAutoCompleteOption|VariableNameAutoCompleteOption|VariableOperatorAutoCompleteOption|VariableValueContextAutoCompleteOption|SimpleAutoCompleteOption)} AnyMacroAutoCompleteOption */
61+
6062/**
6163 * Finds unclosed scoped macros in the text up to cursor position.
6264 * Uses the MacroParser and MacroCstWalker for accurate analysis.
@@ -136,11 +138,11 @@ export function findUnclosedScopesRegex(text) {
136138 * @param {Object} [opts] - Optional configuration.
137139 * @param {boolean} [opts.forIfCondition=false] - If true, options are for {{if}} condition (closes with }}).
138140 * @param {string} [opts.paddingAfter=''] - Whitespace to add before closing }}.
139141 * @returns {(EnhancedMacroAutoCompleteOption|MacroFlagAutoCompleteOption|MacroClosingTagAutoCompleteOption|VariableShorthandAutoCompleteOption|VariableNameAutoCompleteOption|VariableOperatorAutoCompleteOption)AnyMacroAutoCompleteOption[]}
140142 */
141143export function buildVariableShorthandOptions(context, opts = {}) {
142144 const { forIfCondition = false, paddingAfter = '' } = opts;
143145 /** @type {(VariableShorthandAutoCompleteOption|VariableNameAutoCompleteOption|VariableOperatorAutoCompleteOption|VariableValueContextAutoCompleteOption)AnyMacroAutoCompleteOption[]} */
144146 const options = [];
145147
146148 const isLocal = context.variablePrefix === '.';
@@ -276,7 +278,7 @@ export function buildVariableShorthandOptions(context, opts = {}) {
276278
277279 // If typing value (after = or +=), no autocomplete needed - freeform text
278280 // But we show the current context for reference (greyed out, non-selectable)
279281 if (context.isTypingValue && !context.isTypingOperator && !context.isTypingClosingBrace) {
280282 // Show the current variable name as context (non-selectable)
281283 const varNameOption = new VariableNameAutoCompleteOption(context.variableName, scope, false);
282284 varNameOption.valueProvider = () => ''; // Context only
@@ -331,6 +333,49 @@ export function buildVariableShorthandOptions(context, opts = {}) {
331333 }
332334 }
333335
336+ // If typing closing brace on a variable shorthand (without operator), show the current state
337+ // This handles cases like {{.Lila} or {{.Lila}}| where we want to show what was typed
338+ if (context.isTypingClosingBrace && !context.isOperatorComplete && !context.isTypingOperator && !context.isTypingValue) {
339+ // Show the current variable name as context (non-selectable)
340+ const varNameOption = new VariableNameAutoCompleteOption(context.variableName, scope, false);
341+ varNameOption.valueProvider = () => ''; // Context only
342+ varNameOption.makeSelectable = false;
343+ varNameOption.sortPriority = 2;
344+ varNameOption.matchProvider = () => true; // Always show
345+ options.push(varNameOption);
346+ }
347+
348+ // If typing closing brace after a value operator (like {{.Lila+=4}} or {{.Lila+=4}),
349+ // show the full context (variable + operator + value)
350+ if (context.isTypingClosingBrace && context.variableOperator && context.isTypingValue) {
351+ // Show the current variable name as context (non-selectable)
352+ const varNameOption = new VariableNameAutoCompleteOption(context.variableName, scope, false);
353+ varNameOption.valueProvider = () => ''; // Context only
354+ varNameOption.makeSelectable = false;
355+ varNameOption.sortPriority = 2;
356+ varNameOption.matchProvider = () => true; // Always show
357+ options.push(varNameOption);
358+
359+ // Show the operator that was used (non-selectable)
360+ const opDef = VariableOperatorDefinitions.get(context.variableOperator);
361+ if (opDef) {
362+ const opOption = new VariableOperatorAutoCompleteOption(opDef);
363+ opOption.valueProvider = () => ''; // Already typed
364+ opOption.makeSelectable = false;
365+ opOption.sortPriority = 3;
366+ opOption.matchProvider = () => true; // Always show
367+ options.push(opOption);
368+
369+ // Show value context info (non-selectable)
370+ const valueOption = new VariableValueContextAutoCompleteOption(opDef, context.variableValue);
371+ valueOption.valueProvider = () => ''; // Context only
372+ valueOption.makeSelectable = false;
373+ valueOption.sortPriority = 4;
374+ valueOption.matchProvider = () => true; // Always show
375+ options.push(valueOption);
376+ }
377+ }
378+
334379 return options;
335380}
336381
@@ -340,10 +385,10 @@ export function buildVariableShorthandOptions(context, opts = {}) {
340385 * When typing arguments (after ::), prioritizes the exact macro match.
341386 * @param {MacroAutoCompleteContext} context
342387 * @param {string} [textUpToCursor] - Full document text up to cursor, for unclosed scope detection.
343388 * @returns {(EnhancedMacroAutoCompleteOption|MacroFlagAutoCompleteOption|MacroClosingTagAutoCompleteOption|VariableShorthandAutoCompleteOption|VariableNameAutoCompleteOption|VariableOperatorAutoCompleteOption)AnyMacroAutoCompleteOption[]}
344389 */
345390export function buildEnhancedMacroOptions(context, textUpToCursor) {
346391 /** @type {(EnhancedMacroAutoCompleteOption|MacroFlagAutoCompleteOption|MacroClosingTagAutoCompleteOption|VariableShorthandAutoCompleteOption|VariableNameAutoCompleteOption|VariableOperatorAutoCompleteOption)AnyMacroAutoCompleteOption[]} */
347392 const options = [];
348393
349394 if (context.isVariableShorthand) {
@@ -427,16 +472,26 @@ export function buildEnhancedMacroOptions(context, textUpToCursor) {
427472 const allMacros = macroSystem.registry.getAllMacros({ excludeHiddenAliases: true });
428473
429474 // If we're typing arguments (after ::), only show the context to the matching macro
475+ // Also treat typing closing brace the same way - show details for matching macro
430476 const isTypingArgs = context.currentArgIndex >= 0;
477+ const isTypingClosingBrace = context.isTypingClosingBrace ?? false;
478+ const shouldShowMatchingMacroDetails = isTypingArgs || isTypingClosingBrace;
431479
432480 // Check if we're inside a scoped {{if}} for {{else}} selectability
433481 const isInsideScopedIf = unclosedScopes.some(scope => scope.name === 'if');
434482
483+ // Track if any macro matches the identifier (for "no match" message)
484+ let hasMatchingMacro = false;
485+
435486 for (const macro of allMacros) {
436487 // Check if this macro matches the typed identifier
437488 const isExactMatch = macro.name === context.identifier;
438489 const isAliasMatch = macro.aliasOf === context.identifier;
439490
491+ if (isExactMatch || isAliasMatch) {
492+ hasMatchingMacro = true;
493+ }
494+
440495 // Only pass context to the macro that matches the identifier being typed
441496 // This ensures argument hints only show for the relevant macro
442497 /** @type {MacroAutoCompleteContext|EnhancedMacroAutoCompleteOptions|null} */
@@ -461,14 +516,30 @@ export function buildEnhancedMacroOptions(context, textUpToCursor) {
461516 option.makeSelectable = false;
462517 }
463518
464519 // When typing arguments or closing brace, prioritize exact matches by putting them first
465520 if (isTypingArgsshouldShowMatchingMacroDetails && (isExactMatch || isAliasMatch)) {
466521 options.unshift(option);
467522 } else {
468523 options.push(option);
469524 }
470525 }
471526
527+ // If typing args/closing brace but no macro matches, add a "no match" indicator
528+ if (shouldShowMatchingMacroDetails && !hasMatchingMacro && context.identifier.length > 0) {
529+ const noMatchOption = new SimpleAutoCompleteOption({
530+ name: context.identifier,
531+ symbol: '❌',
532+ description: `No macro found: "${context.identifier}"`,
533+ detailedDescription: `The macro name <code>${context.identifier}</code> does not exist.<br><br>Check spelling or use a different macro name.`,
534+ type: 'error',
535+ });
536+ noMatchOption.valueProvider = () => '';
537+ noMatchOption.makeSelectable = false;
538+ noMatchOption.matchProvider = () => true; // Always show
539+ noMatchOption.sortPriority = 0; // Top priority
540+ options.unshift(noMatchOption);
541+ }
542+
472543 return options;
473544}
474545
@@ -600,7 +671,17 @@ export function findMacroAtCursor(text, cursorPos) {
600671 // Search backwards for opening {{ while tracking nesting depth for nested macros
601672 let openPos = -1;
602673 let depth = 0;
603- for (let i = cursorPos - 1; i >= 0; i--) {
674+
675+ // If cursor is right after }}, those are the closing braces of the macro we're looking for,
676+ // not nested braces. Skip them by starting the search before them.
677+ let searchStart = cursorPos - 1;
678+ let cursorAfterClosingBraces = false;
679+ if (cursorPos >= 2 && text[cursorPos - 1] === '}' && text[cursorPos - 2] === '}') {
680+ searchStart = cursorPos - 3; // Start before the }}
681+ cursorAfterClosingBraces = true;
682+ }
683+
684+ for (let i = searchStart; i >= 0; i--) {
604685 if (text[i] === '}' && i > 0 && text[i - 1] === '}') {
605686 // Found }}, going backwards means we're entering a nested macro
606687 depth++;
@@ -624,6 +705,11 @@ export function findMacroAtCursor(text, cursorPos) {
624705
625706 // Search forwards for closing }} while tracking nesting depth
626707 let closePos = -1;
708+
709+ // If cursor is right after }}, we already know where the closing braces are
710+ if (cursorAfterClosingBraces) {
711+ closePos = cursorPos;
712+ } else {
627713 depth = 0;
628714 for (let i = cursorPos; i < text.length - 1; i++) {
629715 if (text[i] === '{' && text[i + 1] === '{') {
@@ -648,6 +734,7 @@ export function findMacroAtCursor(text, cursorPos) {
648734 if (closePos === -1) {
649735 closePos = text.length;
650736 }
737+ }
651738
652739 const hasClosingBraces = closePos <= text.length && text.slice(closePos - 2, closePos) === '}}';
653740 const content = text.slice(openPos + 2, hasClosingBraces ? closePos - 2 : closePos);
@@ -908,7 +995,7 @@ export async function buildMacroAutoCompleteResult(text, cursorPos, {
908995 resultIdentifier = context.invalidTrailingChars || '';
909996 // Use actual variableNameEnd position from parsing
910997 resultStart = macro.start + 2 + context.variableNameEnd;
911998 } else if (context.isTypingValue && !context.isTypingClosingBrace) {
912999 // Typing value: identifier = value being typed, start = after operator
9131000 resultIdentifier = context.variableValue;
9141001 // Use actual operatorEnd position from parsing (accounts for whitespace)
@@ -920,6 +1007,10 @@ export async function buildMacroAutoCompleteResult(text, cursorPos, {
9201007
9211008 makeNoMatchText = () => `Type any value you want to ${context.variableOperator == '+=' ? `add to the variable '${context.variableName}'` : `set the variable '${context.variableName}' to`}.`;
9221009 makeNoOptionsText = () => 'Enter a variable value';
1010+ } else if (context.isTypingClosingBrace) {
1011+ // Typing closing brace on variable shorthand - show context, no replacement needed
1012+ resultIdentifier = '';
1013+ resultStart = cursorPos;
9231014 } else {
9241015 // Fallback: use variable name
9251016 resultIdentifier = context.variableName;