Macros 2.0 - `list`-supported Macros Autocomplete Improvements (#5135) * fix(macros): prevent list-arg macros from accepting scoped content List-arg macros now correctly reject scoped content since they only accept arbitrary inline arguments. Updated validation logic in autocomplete, scope detection, and CST walker to check for `list === null` before allowing scopes. Also improved list item hint display to show total count when typing additional items. * feat(macros): display min/max constraints for list arguments in autocomplete hints Add visual indication of list argument constraints by showing min/max values in autocomplete hints. List items now display "(list, min: X, max: Y)" or "(variable-length list)" when no constraints exist. Includes new CSS styling for smaller, dimmed hint text. * feat(macros): validate list argument min/max constraints and improve warning display Add validation for list argument constraints (min/max) with specific error messages when too few or too many list items are provided. Continue highlighting current argument position even when warnings are present (except where semantically invalid), allowing users to navigate back to valid arguments while seeing "too many arguments" warnings.

953d9f34cbdc33ba062b01baa90dfb7f4c1bce45

Wolfsblvt <wolfsblvt@gmail.com>

Signed
4 files changed, +56 -15Showing whitespace changes
public/css/macros.css+5 -0
@@ -566,6 +566,11 @@
566566 font-size: 0.8em;
567567}
568568
569+.macro-ac-arg-hint .macro-ac-arg-hint-small {
570+ font-size: 0.85em;
571+ opacity: 0.8;
572+}
573+
569574.macro-ac-hint-type {
570575 font-family: var(--monoFontFamily);
571576 font-size: 0.85em;
public/scripts/autocomplete/EnhancedMacroAutoCompleteOption.js+44 -7
@@ -223,15 +223,20 @@ export class EnhancedMacroAutoCompleteOption extends AutoCompleteOption {
223223 // Determine current argument index for highlighting
224224 const currentArgIndex = this.#context?.currentArgIndex ?? -1;
225225
226226 // RenderFor argumentmost hintwarnings, bannerwe ifcan we'restill typinghighlight anwhich argument (andwe noare warning)currently at.
227- if (!warning && currentArgIndex >= 0) {
227+ // This even goes for "too many arguments" when navigating the cursor back to
228+ // a valid argument.
229+ // Extend this in the future, if *some* warnings don't make sense to still highlight args.
230+ const hightlightArgsHint = currentArgIndex >= 0;
231+
232+ // Render argument hint banner if we're typing an argument
233+ if (hightlightArgsHint && currentArgIndex >= 0) {
228234 const hint = this.#renderArgumentHint();
229235 if (hint) frag.append(hint);
230236 }
231237
232238 // Reuse MacroBrowser's renderMacroDetails with options
233- // Don't highlight args if there's a warning
239+ const details = renderMacroDetails(this.#macro, { currentArgIndex: hightlightArgsHint ? currentArgIndex : -1 });
234- const details = renderMacroDetails(this.#macro, { currentArgIndex: warning ? -1 : currentArgIndex });
235240
236241 // Add class for autocomplete-specific styling overrides
237242 details.classList.add('macro-ac-details');
@@ -261,7 +266,7 @@ export class EnhancedMacroAutoCompleteOption extends AutoCompleteOption {
261266 // Space-separated syntax provides 1 arg; with scoped content you can provide a 2nd arg
262267 // So it's valid for macros with maxArgs <= 2 (or with list args)
263268 if (this.#context.hasSpaceArgContent) {
264269 if (maxArgs === 0 && !hasList) {
265270 return 'This macro does not accept any arguments. Remove the space or use a different macro.';
266271 }
267272 if (!hasList && maxArgs > 2) {
@@ -270,10 +275,27 @@ export class EnhancedMacroAutoCompleteOption extends AutoCompleteOption {
270275 }
271276
272277 // Check if trying to add args to a no-arg macro via ::
273- if (this.#context.separatorCount > 0 && maxArgs === 0) {
278+ // List-arg macros can accept args even if maxArgs === 0
279+ if (this.#context.separatorCount > 0 && maxArgs === 0 && !hasList) {
274280 return 'This macro does not accept any arguments.';
275281 }
276282
283+ // Check list bounds (min/max) if the macro has a list with constraints
284+ if (hasList && typeof this.#macro.list === 'object') {
285+ const listItemCount = Math.max(0, argCount - maxArgs);
286+ const listMin = this.#macro.list.min ?? 0;
287+ const listMax = this.#macro.list.max ?? null;
288+
289+ if (listItemCount < listMin) {
290+ const needed = listMin - listItemCount;
291+ return `Not enough list items yet: this macro requires at least ${listMin} item${listMin === 1 ? '' : 's'}, but only ${listItemCount} provided. Add ${needed} more.`;
292+ }
293+
294+ if (listMax !== null && listItemCount > listMax) {
295+ return `Too many list items: this macro accepts at most ${listMax} item${listMax === 1 ? '' : 's'}, but ${listItemCount} provided.`;
296+ }
297+ }
298+
277299 return null;
278300 }
279301
@@ -353,8 +375,23 @@ export class EnhancedMacroAutoCompleteOption extends AutoCompleteOption {
353375 if (isListArg) {
354376 // List argument hint
355377 const listIndex = argIndex - this.#macro.maxArgs + 1;
378+ const totalListItems = this.#context.args.length - this.#macro.maxArgs;
379+
356380 const text = document.createElement('span');
357381 text.innerHTML = `<strong>List item ${listIndex}</strong>${(listIndex < totalListItems ? ` (of ${totalListItems})` : '')}`;
382+
383+ const listInfo = document.createElement('span');
384+ listInfo.classList.add('macro-ac-arg-hint-small');
385+ const minMax = [];
386+ if (this.#macro.list.min > 0) minMax.push(`min: ${this.#macro.list.min}`);
387+ if (this.#macro.list.max !== null) minMax.push(`max: ${this.#macro.list.max}`);
388+ if (minMax.length > 0) {
389+ listInfo.textContent = ` (list, ${minMax.join(', ')})`;
390+ } else {
391+ listInfo.textContent = ' (variable-length list)';
392+ }
393+ text.appendChild(listInfo);
394+
358395 hint.append(text);
359396 } else {
360397 // Unnamed argument hint (required or optional)
public/scripts/autocomplete/MacroAutoCompleteHelper.js+2 -1
@@ -112,8 +112,9 @@ export function findUnclosedScopesRegex(text) {
112112 }
113113 } else {
114114 // Check if macro can accept scoped content
115+ // List-arg macros don't support scopes - they accept arbitrary inline args instead
115116 const macroDef = macroSystem.registry.getPrimaryMacro(name);
116117 if (macroDef && macroDef.maxArgs > 0 && macroDef.list === null) {
117118 // Try to find closing }} to extract trailing whitespace
118119 let paddingAfter = '';
119120 const afterMatch = text.slice(match.index + match[0].length);
public/scripts/macros/engine/MacroCstWalker.js+5 -7
@@ -1309,16 +1309,14 @@ class MacroCstWalker {
13091309 const argumentNodes = /** @type {CstNode[]} */ (argumentsNode?.children?.argument || []);
13101310 const currentArgCount = argumentNodes.length;
13111311
1312- // Check if adding 1 more argument (scoped content) would be valid
1312+ // List-arg macros don't support scoped content - they accept arbitrary inline args instead
1313- const newArgCount = currentArgCount + 1;
1314-
1315- // Macro must accept at least newArgCount arguments
1316- // For macros with list args, they can accept unlimited after maxArgs
13171313 if (def.list) {
1318- // With list: valid if newArgCount >= minArgs (list can absorb extra)
1314+ return false;
1319- return newArgCount >= def.minArgs;
13201315 }
13211316
1317+ // Check if adding 1 more argument (scoped content) would be valid
1318+ const newArgCount = currentArgCount + 1;
1319+
13221320 // Without list: newArgCount must be between minArgs and maxArgs
13231321 return newArgCount >= def.minArgs && newArgCount <= def.maxArgs;
13241322 }