Macros 2.0 [Fix] - Macro display override uses alias name if relevant + preserve whitespaces for autocomplete closing tag (#4953) * Refactor: Move MacroBrowser.js to macros/engine directory and update import paths * Replace macro name with alias in displayOverride for macro browser - Import escapeRegex utility for safe regex pattern creation - Check if macro has aliasOf property when displayOverride is present - Replace all occurrences of the original macro name with the alias name in displayOverride text - Use word boundary regex pattern to match macro names in {{macro}}, {{macro::}}, and {{macro }} contexts * Add whitespace padding preservation for macro closing tag autocomplete - Extract paddingBefore and paddingAfter from opening macro tags in MacroCstWalker - Pass padding info through unclosed scope detection in findUnclosedScopes() and findUnclosedScopesRegex() - Update MacroClosingTagAutoCompleteOption constructor to accept padding options - Apply same whitespace padding to closing tags as their corresponding opening tags - Update regex fallback to capture and preserve whitespace from opening tags * Add replacementStartOffset to AutoCompleteOption for whitespace normalization in closing tags - Add replacementStartOffset property to AutoCompleteOption class with JSDoc documentation - Update AutoComplete.select() to apply per-option replacement offset when calculating effectiveStart - Calculate replacementStartOffset in MacroClosingTagAutoCompleteOption based on currentPadding length - Pass currentPadding from context.paddingBefore to MacroClosingTagAutoCompleteOption in SlashCommandParser * Fix closing tag detection regex to correctly autocomplete // macro * Add flags and fullText to EnhancedMacroAutoCompleteOption context and fix {{//}} autocomplete offset - Add flags, currentFlag, and fullText properties to EnhancedMacroAutoCompleteOptions JSDoc - Store options object as #options field in EnhancedMacroAutoCompleteOption for later access - Pass flags, currentFlag, and fullText from context to macroContext in SlashCommandParser - Fix {{//}} macro autocomplete to adjust replacementStartOffset when typed after single slash to avoid treating it as flag * Add unnamedArgs definition to {{//}} comment macro for better documentation --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>
Signed| @@ -692,8 +692,10 @@ export class AutoComplete { | |||
| 692 | */ | 692 | */ |
| 693 | async select() { | 693 | async select() { |
| 694 | if (this.isReplaceable && this.selectedItem.value !== null) { | 694 | if (this.isReplaceable && this.selectedItem.value !== null) { |
| 695 | this.textarea.value = `${this.text.slice(0, this.effectiveParserResult.start)}${this.selectedItem.replacer}${this.text.slice(this.effectiveParserResult.start + this.effectiveParserResult.name.length + (this.startQuote ? 1 : 0) + (this.endQuote ? 1 : 0))}`; | 695 | // Apply per-option replacement offset (e.g., for closing tags that need to replace leading whitespace) |
| 696 | this.textarea.selectionStart = this.effectiveParserResult.start + this.selectedItem.replacer.length; | 696 | const effectiveStart = this.effectiveParserResult.start + (this.selectedItem.replacementStartOffset ?? 0); |
| 697 | this.textarea.value = `${this.text.slice(0, effectiveStart)}${this.selectedItem.replacer}${this.text.slice(this.effectiveParserResult.start + this.effectiveParserResult.name.length + (this.startQuote ? 1 : 0) + (this.endQuote ? 1 : 0))}`; | ||
| 698 | this.textarea.selectionStart = effectiveStart + this.selectedItem.replacer.length; | ||
| 697 | this.textarea.selectionEnd = this.textarea.selectionStart; | 699 | this.textarea.selectionEnd = this.textarea.selectionStart; |
| 698 | this.show(false, false, true); | 700 | this.show(false, false, true); |
| 699 | } else { | 701 | } else { |
| @@ -13,6 +13,15 @@ export class AutoCompleteOption { | |||
| 13 | /** @type {(input:string)=>boolean} */ matchProvider; | 13 | /** @type {(input:string)=>boolean} */ matchProvider; |
| 14 | /** @type {(input:string)=>string} */ valueProvider; | 14 | /** @type {(input:string)=>string} */ valueProvider; |
| 15 | /** @type {boolean} */ makeSelectable = false; | 15 | /** @type {boolean} */ makeSelectable = false; |
| 16 | |||
| 17 | /** | ||
| 18 | * Offset to adjust the replacement start position. | ||
| 19 | * Negative values start replacement earlier (e.g., -2 to include 2 chars before normal start). | ||
| 20 | * Used by closing tag autocomplete to replace leading whitespace. | ||
| 21 | * @type {number} | ||
| 22 | */ | ||
| 23 | replacementStartOffset = 0; | ||
| 24 | |||
| 16 | /** | 25 | /** |
| 17 | * Priority for sorting. Lower values = higher priority (sorted first). | 26 | * Priority for sorting. Lower values = higher priority (sorted first). |
| 18 | * Default is 100 (normal priority). Use lower values for items that should appear at the top. | 27 | * Default is 100 (normal priority). Use lower values for items that should appear at the top. |
| @@ -9,7 +9,7 @@ import { | |||
| 9 | createSourceIndicator, | 9 | createSourceIndicator, |
| 10 | createAliasIndicator, | 10 | createAliasIndicator, |
| 11 | renderMacroDetails, | 11 | renderMacroDetails, |
| 12 | } from '../macros/MacroBrowser.js'; | 12 | } from '../macros/engine/MacroBrowser.js'; |
| 13 | import { enumIcons } from '../slash-commands/SlashCommandCommonEnumsProvider.js'; | 13 | import { enumIcons } from '../slash-commands/SlashCommandCommonEnumsProvider.js'; |
| 14 | import { ValidFlagSymbols } from '../macros/engine/MacroFlags.js'; | 14 | import { ValidFlagSymbols } from '../macros/engine/MacroFlags.js'; |
| 15 | import { MACRO_VARIABLE_SHORTHAND_PATTERN } from '../macros/engine/MacroLexer.js'; | 15 | import { MACRO_VARIABLE_SHORTHAND_PATTERN } from '../macros/engine/MacroLexer.js'; |
| @@ -54,6 +54,9 @@ import { MACRO_VARIABLE_SHORTHAND_PATTERN } from '../macros/engine/MacroLexer.js | |||
| 54 | * @property {boolean} [noBraces=false] - If true, display without {{ }} braces (for use as values, e.g., in {{if}} conditions). | 54 | * @property {boolean} [noBraces=false] - If true, display without {{ }} braces (for use as values, e.g., in {{if}} conditions). |
| 55 | * @property {string} [paddingAfter=''] - Whitespace to add before closing }} (for matching opening whitespace style). | 55 | * @property {string} [paddingAfter=''] - Whitespace to add before closing }} (for matching opening whitespace style). |
| 56 | * @property {boolean} [closeWithBraces=false] - If true, the completion will add }} to close the macro. | 56 | * @property {boolean} [closeWithBraces=false] - If true, the completion will add }} to close the macro. |
| 57 | * @property {string[]} [flags=[]] - The currently already written flags for this autocomplete. | ||
| 58 | * @property {string} [currentFlag] - The current flag that is present, if any. | ||
| 59 | * @property {string} [fullText] - The currently written full text. | ||
| 57 | */ | 60 | */ |
| 58 | 61 | ||
| 59 | export class EnhancedMacroAutoCompleteOption extends AutoCompleteOption { | 62 | export class EnhancedMacroAutoCompleteOption extends AutoCompleteOption { |
| @@ -63,6 +66,9 @@ export class EnhancedMacroAutoCompleteOption extends AutoCompleteOption { | |||
| 63 | /** @type {MacroAutoCompleteContext|null} */ | 66 | /** @type {MacroAutoCompleteContext|null} */ |
| 64 | #context = null; | 67 | #context = null; |
| 65 | 68 | ||
| 69 | /** @type {EnhancedMacroAutoCompleteOptions|null} */ | ||
| 70 | #options = null; | ||
| 71 | |||
| 66 | /** @type {boolean} */ | 72 | /** @type {boolean} */ |
| 67 | #noBraces = false; | 73 | #noBraces = false; |
| 68 | 74 | ||
| @@ -83,12 +89,12 @@ export class EnhancedMacroAutoCompleteOption extends AutoCompleteOption { | |||
| 83 | if (contextOrOptions && typeof contextOrOptions === 'object') { | 89 | if (contextOrOptions && typeof contextOrOptions === 'object') { |
| 84 | if ('noBraces' in contextOrOptions || 'paddingAfter' in contextOrOptions || 'closeWithBraces' in contextOrOptions) { | 90 | if ('noBraces' in contextOrOptions || 'paddingAfter' in contextOrOptions || 'closeWithBraces' in contextOrOptions) { |
| 85 | // It's an options object | 91 | // It's an options object |
| 86 | const options = /** @type {EnhancedMacroAutoCompleteOptions} */ (contextOrOptions); | 92 | this.#options = /** @type {EnhancedMacroAutoCompleteOptions} */ (contextOrOptions); |
| 87 | this.#noBraces = options.noBraces ?? false; | 93 | this.#noBraces = this.#options.noBraces ?? false; |
| 88 | this.#paddingAfter = options.paddingAfter ?? ''; | 94 | this.#paddingAfter = this.#options.paddingAfter ?? ''; |
| 89 | 95 | ||
| 90 | // If noBraces mode with closeWithBraces, complete with name + padding + }} | 96 | // If noBraces mode with closeWithBraces, complete with name + padding + }} |
| 91 | if (options.closeWithBraces) { | 97 | if (this.#options.closeWithBraces) { |
| 92 | this.valueProvider = () => `${macro.name}${this.#paddingAfter}}}`; | 98 | this.valueProvider = () => `${macro.name}${this.#paddingAfter}}}`; |
| 93 | this.makeSelectable = true; | 99 | this.makeSelectable = true; |
| 94 | } | 100 | } |
| @@ -110,6 +116,12 @@ export class EnhancedMacroAutoCompleteOption extends AutoCompleteOption { | |||
| 110 | this.makeSelectable = true; // Required when using valueProvider | 116 | this.makeSelectable = true; // Required when using valueProvider |
| 111 | } | 117 | } |
| 112 | } | 118 | } |
| 119 | |||
| 120 | // {{//}} needs special handling. If we autocomplete right after **one** slash is already typed, we need to replace that, as it's treated as a flag otherwise. | ||
| 121 | const fullText = this.#options?.fullText ?? this.#context?.fullText ?? ''; | ||
| 122 | if (macro.name === '//' && fullText.endsWith('/')) { | ||
| 123 | this.replacementStartOffset = (this.replacementStartOffset ?? 0) - 1; // Cut the leading slash | ||
| 124 | } | ||
| 113 | } | 125 | } |
| 114 | 126 | ||
| 115 | /** @returns {MacroDefinition} */ | 127 | /** @returns {MacroDefinition} */ |
| @@ -907,20 +919,38 @@ export class MacroClosingTagAutoCompleteOption extends AutoCompleteOption { | |||
| 907 | /** @type {string} */ | 919 | /** @type {string} */ |
| 908 | #macroName; | 920 | #macroName; |
| 909 | 921 | ||
| 922 | /** @type {string} */ | ||
| 923 | #paddingBefore; | ||
| 924 | |||
| 925 | /** @type {string} */ | ||
| 926 | #paddingAfter; | ||
| 927 | |||
| 910 | /** | 928 | /** |
| 911 | * @param {string} macroName - The name of the macro to close. | 929 | * @param {string} macroName - The name of the macro to close. |
| 930 | * @param {Object} [options] - Optional configuration. | ||
| 931 | * @param {string} [options.paddingBefore=''] - Whitespace after {{ in opening tag (target padding). | ||
| 932 | * @param {string} [options.paddingAfter=''] - Whitespace before }} in opening tag (target padding). | ||
| 933 | * @param {string} [options.currentPadding=''] - Whitespace the user has already typed after {{. | ||
| 912 | */ | 934 | */ |
| 913 | constructor(macroName) { | 935 | constructor(macroName, options = {}) { |
| 914 | // The closing tag is what we're suggesting - use /macroName as the name for matching | 936 | // The closing tag is what we're suggesting - use /macroName as the name for matching |
| 915 | const closingTag = `/${macroName}`; | 937 | const closingTag = `/${macroName}`; |
| 916 | super(closingTag, '{/'); | 938 | super(closingTag, '{/'); |
| 917 | this.#macroName = macroName; | 939 | this.#macroName = macroName; |
| 940 | this.#paddingBefore = options.paddingBefore ?? ''; | ||
| 941 | this.#paddingAfter = options.paddingAfter ?? ''; | ||
| 942 | |||
| 943 | // Calculate the replacement offset to replace any existing whitespace the user typed | ||
| 944 | // This allows us to normalize the whitespace to match the opening tag's style | ||
| 945 | const currentPadding = options.currentPadding ?? ''; | ||
| 946 | // Negative offset to start replacement earlier (eating the user's whitespace) | ||
| 947 | this.replacementStartOffset = -currentPadding.length; | ||
| 918 | 948 | ||
| 919 | // Custom valueProvider to return the correct replacement text | 949 | // Custom valueProvider to return the correct replacement text |
| 920 | // Autocomplete REPLACES the typed identifier entirely, so return the full closing tag | 950 | // Includes the target paddingBefore from the opening tag, replacing any user-typed whitespace |
| 921 | this.valueProvider = () => { | 951 | this.valueProvider = () => { |
| 922 | // Return full closing tag content (without {{ since that's before the identifier) | 952 | // Return: paddingBefore + /macroName + paddingAfter + }} |
| 923 | return `/${macroName}}}`; | 953 | return `${this.#paddingBefore}/${macroName}${this.#paddingAfter}}}`; |
| 924 | }; | 954 | }; |
| 925 | 955 | ||
| 926 | // Make selectable so TAB completion works (valueProvider alone makes it non-selectable) | 956 | // Make selectable so TAB completion works (valueProvider alone makes it non-selectable) |
| @@ -1033,7 +1063,7 @@ export function parseMacroContext(macroText, cursorOffset) { | |||
| 1033 | while (i < macroText.length) { | 1063 | while (i < macroText.length) { |
| 1034 | const char = macroText[i]; | 1064 | const char = macroText[i]; |
| 1035 | // Check if this looks like a closing tag: `/` followed by an identifier character | 1065 | // Check if this looks like a closing tag: `/` followed by an identifier character |
| 1036 | if (char === '/' && i + 1 < macroText.length && /[a-zA-Z_]/.test(macroText[i + 1])) { | 1066 | if (char === '/' && i + 1 < macroText.length && /[a-zA-Z/]/.test(macroText[i + 1])) { |
| 1037 | // This is a closing tag identifier, not a flag - stop parsing flags | 1067 | // This is a closing tag identifier, not a flag - stop parsing flags |
| 1038 | break; | 1068 | break; |
| 1039 | } | 1069 | } |
| @@ -261,6 +261,13 @@ export function registerCoreMacros() { | |||
| 261 | MacroRegistry.registerMacro('//', { | 261 | MacroRegistry.registerMacro('//', { |
| 262 | aliases: [{ alias: 'comment', visible: false }], | 262 | aliases: [{ alias: 'comment', visible: false }], |
| 263 | category: MacroCategory.UTILITY, | 263 | category: MacroCategory.UTILITY, |
| 264 | unnamedArgs: [ | ||
| 265 | { | ||
| 266 | name: 'comment', | ||
| 267 | type: MacroValueType.STRING, | ||
| 268 | description: 'Any kind of text as comment. If you want multiline comments, consider using a scoped macro like {{//}}First\nSecond{{///}}.', | ||
| 269 | }, | ||
| 270 | ], | ||
| 264 | list: true, // We consume any arguments as if this is a list, but we'll ignore them in the handler anyway | 271 | list: true, // We consume any arguments as if this is a list, but we'll ignore them in the handler anyway |
| 265 | strictArgs: false, // and we also always remove it, even if the parsing might say it's invalid | 272 | strictArgs: false, // and we also always remove it, even if the parsing might say it's invalid |
| 266 | description: 'Comment macro that produces an empty string. Can be used for writing into prompt definitions, without being passed to the context.', | 273 | description: 'Comment macro that produces an empty string. Can be used for writing into prompt definitions, without being passed to the context.', |
| @@ -3,11 +3,12 @@ | |||
| 3 | * Similar to SlashCommandBrowser but for the macro system. | 3 | * Similar to SlashCommandBrowser but for the macro system. |
| 4 | */ | 4 | */ |
| 5 | 5 | ||
| 6 | import { MacroRegistry, MacroCategory } from './engine/MacroRegistry.js'; | 6 | import { MacroRegistry, MacroCategory } from './MacroRegistry.js'; |
| 7 | import { performFuzzySearch } from '../power-user.js'; | 7 | import { performFuzzySearch } from '../../power-user.js'; |
| 8 | import { escapeRegex } from '/scripts/utils.js'; | ||
| 8 | 9 | ||
| 9 | /** @typedef {import('./engine/MacroRegistry.js').MacroDefinition} MacroDefinition */ | 10 | /** @typedef {import('./MacroRegistry.js').MacroDefinition} MacroDefinition */ |
| 10 | /** @typedef {import('./engine/MacroRegistry.js').MacroValueType} MacroValueType */ | 11 | /** @typedef {import('./MacroRegistry.js').MacroValueType} MacroValueType */ |
| 11 | 12 | ||
| 12 | /** | 13 | /** |
| 13 | * Category display names and order for documentation. | 14 | * Category display names and order for documentation. |
| @@ -328,6 +329,11 @@ function getCategoryConfig(category) { | |||
| 328 | export function formatMacroSignature(macro) { | 329 | export function formatMacroSignature(macro) { |
| 329 | // Use displayOverride if provided | 330 | // Use displayOverride if provided |
| 330 | if (macro.displayOverride) { | 331 | if (macro.displayOverride) { |
| 332 | if (macro.aliasOf) { | ||
| 333 | // Replace all occurrences of the macro name with the alias for this list | ||
| 334 | const escapedMainName = escapeRegex(macro.aliasOf); | ||
| 335 | return macro.displayOverride.replace(new RegExp(`(?<=[\\b{\\s])${escapedMainName}(?=[\\b}:\\s])`, 'g'), `${macro.name}`); | ||
| 336 | } | ||
| 331 | return macro.displayOverride; | 337 | return macro.displayOverride; |
| 332 | } | 338 | } |
| 333 | 339 | ||
| @@ -190,7 +190,7 @@ class MacroCstWalker { | |||
| 190 | * @param {Object} options | 190 | * @param {Object} options |
| 191 | * @param {string} options.text - The document text. | 191 | * @param {string} options.text - The document text. |
| 192 | * @param {CstNode} options.cst - The parsed CST. | 192 | * @param {CstNode} options.cst - The parsed CST. |
| 193 | * @returns {Array<{ name: string, startOffset: number, endOffset: number }>} - Array of unclosed macro info, innermost last. | 193 | * @returns {Array<{ name: string, startOffset: number, endOffset: number, paddingBefore: string, paddingAfter: string }>} - Array of unclosed macro info, innermost last. |
| 194 | */ | 194 | */ |
| 195 | findUnclosedScopes(options) { | 195 | findUnclosedScopes(options) { |
| 196 | const { text, cst } = options; | 196 | const { text, cst } = options; |
| @@ -203,7 +203,7 @@ class MacroCstWalker { | |||
| 203 | // Don't process scoped macros - we want to find the raw opening/closing pairs | 203 | // Don't process scoped macros - we want to find the raw opening/closing pairs |
| 204 | // Just extract macro info and find unmatched openers | 204 | // Just extract macro info and find unmatched openers |
| 205 | 205 | ||
| 206 | /** @type {Array<{ name: string, startOffset: number, endOffset: number }>} */ | 206 | /** @type {Array<{ name: string, startOffset: number, endOffset: number, paddingBefore: string, paddingAfter: string }>} */ |
| 207 | const unclosedStack = []; | 207 | const unclosedStack = []; |
| 208 | 208 | ||
| 209 | // Extract macro names and closing status | 209 | // Extract macro names and closing status |
| @@ -222,10 +222,15 @@ class MacroCstWalker { | |||
| 222 | } else { | 222 | } else { |
| 223 | // Opening tag - check if this macro can accept scoped content | 223 | // Opening tag - check if this macro can accept scoped content |
| 224 | if (this.#canAcceptScopedContent(item.node, info.name)) { | 224 | if (this.#canAcceptScopedContent(item.node, info.name)) { |
| 225 | // Extract whitespace padding from the macro | ||
| 226 | const { paddingBefore, paddingAfter } = this.#extractMacroPadding(item.node, text); | ||
| 227 | |||
| 225 | unclosedStack.push({ | 228 | unclosedStack.push({ |
| 226 | name: info.name, | 229 | name: info.name, |
| 227 | startOffset: item.startOffset, | 230 | startOffset: item.startOffset, |
| 228 | endOffset: item.endOffset, | 231 | endOffset: item.endOffset, |
| 232 | paddingBefore, | ||
| 233 | paddingAfter, | ||
| 229 | }); | 234 | }); |
| 230 | } | 235 | } |
| 231 | } | 236 | } |
| @@ -234,6 +239,39 @@ class MacroCstWalker { | |||
| 234 | return unclosedStack; | 239 | return unclosedStack; |
| 235 | } | 240 | } |
| 236 | 241 | ||
| 242 | /** | ||
| 243 | * Extracts the whitespace padding from a macro node. | ||
| 244 | * Returns the whitespace after {{ and before }}. | ||
| 245 | * | ||
| 246 | * @param {CstNode} macroNode - The macro CST node. | ||
| 247 | * @param {string} text - The source text. | ||
| 248 | * @returns {{ paddingBefore: string, paddingAfter: string }} | ||
| 249 | */ | ||
| 250 | #extractMacroPadding(macroNode, text) { | ||
| 251 | const children = macroNode.children || {}; | ||
| 252 | const startToken = /** @type {IToken?} */ ((children['Macro.Start'] || [])[0]); | ||
| 253 | const endToken = /** @type {IToken?} */ ((children['Macro.End'] || [])[0]); | ||
| 254 | |||
| 255 | if (!startToken || !endToken) { | ||
| 256 | return { paddingBefore: '', paddingAfter: '' }; | ||
| 257 | } | ||
| 258 | |||
| 259 | // Get the raw text inside the macro (between {{ and }}) | ||
| 260 | const innerStart = startToken.endOffset + 1; | ||
| 261 | const innerEnd = endToken.startOffset; | ||
| 262 | const innerText = text.slice(innerStart, innerEnd); | ||
| 263 | |||
| 264 | // Extract leading whitespace (paddingBefore) | ||
| 265 | const leadingMatch = innerText.match(/^(\s*)/); | ||
| 266 | const paddingBefore = leadingMatch ? leadingMatch[1] : ''; | ||
| 267 | |||
| 268 | // Extract trailing whitespace (paddingAfter) | ||
| 269 | const trailingMatch = innerText.match(/(\s*)$/); | ||
| 270 | const paddingAfter = trailingMatch ? trailingMatch[1] : ''; | ||
| 271 | |||
| 272 | return { paddingBefore, paddingAfter }; | ||
| 273 | } | ||
| 274 | |||
| 237 | /** @typedef {{ type: 'plaintext', startOffset: number, endOffset: number, token: IToken }} DocumentItemPlaintext */ | 275 | /** @typedef {{ type: 'plaintext', startOffset: number, endOffset: number, token: IToken }} DocumentItemPlaintext */ |
| 238 | /** @typedef {{ type: 'macro', startOffset: number, endOffset: number, node: CstNode, scopedContent?: { startOffset: number, endOffset: number, closingEndOffset: number }, keepRaw?: boolean }} DocumentItemMacro */ | 276 | /** @typedef {{ type: 'macro', startOffset: number, endOffset: number, node: CstNode, scopedContent?: { startOffset: number, endOffset: number, closingEndOffset: number }, keepRaw?: boolean }} DocumentItemMacro */ |
| 239 | /** @typedef {DocumentItemPlaintext | DocumentItemMacro} DocumentItem */ | 277 | /** @typedef {DocumentItemPlaintext | DocumentItemMacro} DocumentItem */ |
| @@ -808,7 +808,13 @@ export class SlashCommandParser { | |||
| 808 | if (unclosedScopes.length > 0) { | 808 | if (unclosedScopes.length > 0) { |
| 809 | // Suggest closing the innermost (last) unclosed scope first | 809 | // Suggest closing the innermost (last) unclosed scope first |
| 810 | const innermostScope = unclosedScopes[unclosedScopes.length - 1]; | 810 | const innermostScope = unclosedScopes[unclosedScopes.length - 1]; |
| 811 | const closingOption = new MacroClosingTagAutoCompleteOption(innermostScope.name); | 811 | // Preserve whitespace padding from the opening tag |
| 812 | // Pass currentPadding so the closing tag can replace user-typed whitespace with the target padding | ||
| 813 | const closingOption = new MacroClosingTagAutoCompleteOption(innermostScope.name, { | ||
| 814 | paddingBefore: innermostScope.paddingBefore, | ||
| 815 | paddingAfter: innermostScope.paddingAfter, | ||
| 816 | currentPadding: context.paddingBefore, | ||
| 817 | }); | ||
| 812 | options.push(closingOption); | 818 | options.push(closingOption); |
| 813 | 819 | ||
| 814 | // If inside a scoped {{if}}, also suggest {{else}} | 820 | // If inside a scoped {{if}}, also suggest {{else}} |
| @@ -891,6 +897,9 @@ export class SlashCommandParser { | |||
| 891 | if (!macroContext) { | 897 | if (!macroContext) { |
| 892 | macroContext = /** @type {EnhancedMacroAutoCompleteOptions} */ ({ | 898 | macroContext = /** @type {EnhancedMacroAutoCompleteOptions} */ ({ |
| 893 | paddingAfter: context.paddingBefore, // Match whitespace before the macro - will only be used if the macro gets auto-closed | 899 | paddingAfter: context.paddingBefore, // Match whitespace before the macro - will only be used if the macro gets auto-closed |
| 900 | flags: context.flags, | ||
| 901 | currentFlag: context.currentFlag, | ||
| 902 | fullText: context.fullText, | ||
| 894 | }); | 903 | }); |
| 895 | } | 904 | } |
| 896 | 905 | ||
| @@ -1216,7 +1225,7 @@ export class SlashCommandParser { | |||
| 1216 | * Uses the MacroParser and MacroCstWalker for accurate analysis. | 1225 | * Uses the MacroParser and MacroCstWalker for accurate analysis. |
| 1217 | * | 1226 | * |
| 1218 | * @param {string} textUpToCursor - The document text up to the cursor position. | 1227 | * @param {string} textUpToCursor - The document text up to the cursor position. |
| 1219 | * @returns {Array<{ name: string, startOffset: number, endOffset: number }>} | 1228 | * @returns {Array<{ name: string, startOffset: number, endOffset: number, paddingBefore: string, paddingAfter: string }>} |
| 1220 | */ | 1229 | */ |
| 1221 | #findUnclosedScopes(textUpToCursor) { | 1230 | #findUnclosedScopes(textUpToCursor) { |
| 1222 | if (!textUpToCursor) return []; | 1231 | if (!textUpToCursor) return []; |
| @@ -1239,32 +1248,43 @@ export class SlashCommandParser { | |||
| 1239 | * Used when the parser fails on incomplete input. | 1248 | * Used when the parser fails on incomplete input. |
| 1240 | * | 1249 | * |
| 1241 | * @param {string} text - The text to analyze. | 1250 | * @param {string} text - The text to analyze. |
| 1242 | * @returns {Array<{ name: string, startOffset: number, endOffset: number }>} | 1251 | * @returns {Array<{ name: string, startOffset: number, endOffset: number, paddingBefore: string, paddingAfter: string }>} |
| 1243 | */ | 1252 | */ |
| 1244 | #findUnclosedScopesRegex(text) { | 1253 | #findUnclosedScopesRegex(text) { |
| 1245 | // Simple regex to find macro openings and closings | 1254 | // Regex to find macro openings and closings, capturing whitespace padding |
| 1246 | // This is a fallback - less accurate but works on partial input | 1255 | // Group 1: padding after {{, Group 2: optional /, Group 3: macro name |
| 1247 | const macroPattern = /\{\{(\/?)([\w-]+)/g; | 1256 | const macroPattern = /\{\{(\s*)(\/?)([\w-]+)/g; |
| 1248 | const stack = []; | 1257 | const stack = []; |
| 1249 | 1258 | ||
| 1250 | let match; | 1259 | let match; |
| 1251 | while ((match = macroPattern.exec(text)) !== null) { | 1260 | while ((match = macroPattern.exec(text)) !== null) { |
| 1252 | const isClosing = match[1] === '/'; | 1261 | const paddingBefore = match[1]; |
| 1253 | const name = match[2]; | 1262 | const isClosing = match[2] === '/'; |
| 1263 | const name = match[3]; | ||
| 1254 | 1264 | ||
| 1255 | if (isClosing) { | 1265 | if (isClosing) { |
| 1256 | // Pop matching opener | 1266 | // Pop matching opener (case-insensitive) |
| 1257 | if (stack.length > 0 && stack[stack.length - 1].name === name) { | 1267 | if (stack.length > 0 && stack[stack.length - 1].name.toLowerCase() === name.toLowerCase()) { |
| 1258 | stack.pop(); | 1268 | stack.pop(); |
| 1259 | } | 1269 | } |
| 1260 | } else { | 1270 | } else { |
| 1261 | // Check if macro can accept scoped content | 1271 | // Check if macro can accept scoped content |
| 1262 | const macroDef = macroSystem.registry.getPrimaryMacro(name); | 1272 | const macroDef = macroSystem.registry.getPrimaryMacro(name); |
| 1263 | if (macroDef && macroDef.maxArgs > 0) { | 1273 | if (macroDef && macroDef.maxArgs > 0) { |
| 1274 | // Try to find closing }} to extract trailing whitespace | ||
| 1275 | let paddingAfter = ''; | ||
| 1276 | const afterMatch = text.slice(match.index + match[0].length); | ||
| 1277 | const closingMatch = afterMatch.match(/^[^}]*?(\s*)\}\}/); | ||
| 1278 | if (closingMatch) { | ||
| 1279 | paddingAfter = closingMatch[1]; | ||
| 1280 | } | ||
| 1281 | |||
| 1264 | stack.push({ | 1282 | stack.push({ |
| 1265 | name, | 1283 | name, |
| 1266 | startOffset: match.index, | 1284 | startOffset: match.index, |
| 1267 | endOffset: match.index + match[0].length, | 1285 | endOffset: match.index + match[0].length, |
| 1286 | paddingBefore, | ||
| 1287 | paddingAfter, | ||
| 1268 | }); | 1288 | }); |
| 1269 | } | 1289 | } |
| 1270 | } | 1290 | } |
| @@ -4,7 +4,7 @@ import { t } from './i18n.js'; | |||
| 4 | import { getMessageTimeStamp } from './RossAscends-mods.js'; | 4 | import { getMessageTimeStamp } from './RossAscends-mods.js'; |
| 5 | import { getSlashCommandsHelp } from './slash-commands.js'; | 5 | import { getSlashCommandsHelp } from './slash-commands.js'; |
| 6 | import { SlashCommandBrowser } from './slash-commands/SlashCommandBrowser.js'; | 6 | import { SlashCommandBrowser } from './slash-commands/SlashCommandBrowser.js'; |
| 7 | import { MacroBrowser, getMacrosHelp } from './macros/MacroBrowser.js'; | 7 | import { MacroBrowser, getMacrosHelp } from './macros/engine/MacroBrowser.js'; |
| 8 | import { renderTemplateAsync } from './templates.js'; | 8 | import { renderTemplateAsync } from './templates.js'; |
| 9 | 9 | ||
| 10 | /** @type {Record<string, ChatMessage>} */ | 10 | /** @type {Record<string, ChatMessage>} */ |