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 -15Ignore whitespace
public/css/macros.css+5 -0
@@ -566,6 +566,11 @@
566 font-size: 0.8em;566 font-size: 0.8em;
567}567}
568568
569.macro-ac-arg-hint .macro-ac-arg-hint-small {
570 font-size: 0.85em;
571 opacity: 0.8;
572}
573
569.macro-ac-hint-type {574.macro-ac-hint-type {
570 font-family: var(--monoFontFamily);575 font-family: var(--monoFontFamily);
571 font-size: 0.85em;576 font-size: 0.85em;
public/scripts/autocomplete/EnhancedMacroAutoCompleteOption.js+44 -7
@@ -223,15 +223,20 @@ export class EnhancedMacroAutoCompleteOption extends AutoCompleteOption {
223 // Determine current argument index for highlighting223 // Determine current argument index for highlighting
224 const currentArgIndex = this.#context?.currentArgIndex ?? -1;224 const currentArgIndex = this.#context?.currentArgIndex ?? -1;
225225
226 // Render argument hint banner if we're typing an argument (and no warning)226 // For most warnings, we can still highlight which argument we are 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) {
228 const hint = this.#renderArgumentHint();234 const hint = this.#renderArgumentHint();
229 if (hint) frag.append(hint);235 if (hint) frag.append(hint);
230 }236 }
231237
232 // Reuse MacroBrowser's renderMacroDetails with options238 // Reuse MacroBrowser's renderMacroDetails with options
233 // Don't highlight args if there's a warning239 const details = renderMacroDetails(this.#macro, { currentArgIndex: hightlightArgsHint ? currentArgIndex : -1 });
234 const details = renderMacroDetails(this.#macro, { currentArgIndex: warning ? -1 : currentArgIndex });
235240
236 // Add class for autocomplete-specific styling overrides241 // Add class for autocomplete-specific styling overrides
237 details.classList.add('macro-ac-details');242 details.classList.add('macro-ac-details');
@@ -261,7 +266,7 @@ export class EnhancedMacroAutoCompleteOption extends AutoCompleteOption {
261 // Space-separated syntax provides 1 arg; with scoped content you can provide a 2nd arg266 // Space-separated syntax provides 1 arg; with scoped content you can provide a 2nd arg
262 // So it's valid for macros with maxArgs <= 2 (or with list args)267 // So it's valid for macros with maxArgs <= 2 (or with list args)
263 if (this.#context.hasSpaceArgContent) {268 if (this.#context.hasSpaceArgContent) {
264 if (maxArgs === 0) {269 if (maxArgs === 0 && !hasList) {
265 return 'This macro does not accept any arguments. Remove the space or use a different macro.';270 return 'This macro does not accept any arguments. Remove the space or use a different macro.';
266 }271 }
267 if (!hasList && maxArgs > 2) {272 if (!hasList && maxArgs > 2) {
@@ -270,10 +275,27 @@ export class EnhancedMacroAutoCompleteOption extends AutoCompleteOption {
270 }275 }
271276
272 // Check if trying to add args to a no-arg macro via ::277 // 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) {
274 return 'This macro does not accept any arguments.';280 return 'This macro does not accept any arguments.';
275 }281 }
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
277 return null;299 return null;
278 }300 }
279301
@@ -353,8 +375,23 @@ export class EnhancedMacroAutoCompleteOption extends AutoCompleteOption {
353 if (isListArg) {375 if (isListArg) {
354 // List argument hint376 // List argument hint
355 const listIndex = argIndex - this.#macro.maxArgs + 1;377 const listIndex = argIndex - this.#macro.maxArgs + 1;
378 const totalListItems = this.#context.args.length - this.#macro.maxArgs;
379
356 const text = document.createElement('span');380 const text = document.createElement('span');
357 text.innerHTML = `<strong>List item ${listIndex}</strong>`;381 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
358 hint.append(text);395 hint.append(text);
359 } else {396 } else {
360 // Unnamed argument hint (required or optional)397 // Unnamed argument hint (required or optional)
public/scripts/autocomplete/MacroAutoCompleteHelper.js+2 -1
@@ -112,8 +112,9 @@ export function findUnclosedScopesRegex(text) {
112 }112 }
113 } else {113 } else {
114 // Check if macro can accept scoped content114 // Check if macro can accept scoped content
115 // List-arg macros don't support scopes - they accept arbitrary inline args instead
115 const macroDef = macroSystem.registry.getPrimaryMacro(name);116 const macroDef = macroSystem.registry.getPrimaryMacro(name);
116 if (macroDef && macroDef.maxArgs > 0) {117 if (macroDef && macroDef.maxArgs > 0 && macroDef.list === null) {
117 // Try to find closing }} to extract trailing whitespace118 // Try to find closing }} to extract trailing whitespace
118 let paddingAfter = '';119 let paddingAfter = '';
119 const afterMatch = text.slice(match.index + match[0].length);120 const afterMatch = text.slice(match.index + match[0].length);
public/scripts/macros/engine/MacroCstWalker.js+5 -7
@@ -1309,16 +1309,14 @@ class MacroCstWalker {
1309 const argumentNodes = /** @type {CstNode[]} */ (argumentsNode?.children?.argument || []);1309 const argumentNodes = /** @type {CstNode[]} */ (argumentsNode?.children?.argument || []);
1310 const currentArgCount = argumentNodes.length;1310 const currentArgCount = argumentNodes.length;
13111311
1312 // Check if adding 1 more argument (scoped content) would be valid1312 // 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
1317 if (def.list) {1313 if (def.list) {
1318 // With list: valid if newArgCount >= minArgs (list can absorb extra)1314 return false;
1319 return newArgCount >= def.minArgs;
1320 }1315 }
13211316
1317 // Check if adding 1 more argument (scoped content) would be valid
1318 const newArgCount = currentArgCount + 1;
1319
1322 // Without list: newArgCount must be between minArgs and maxArgs1320 // Without list: newArgCount must be between minArgs and maxArgs
1323 return newArgCount >= def.minArgs && newArgCount <= def.maxArgs;1321 return newArgCount >= def.minArgs && newArgCount <= def.maxArgs;
1324 }1322 }