File size: 9,420 Bytes
6efa67a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
/**
 * Enhanced macro autocomplete option for the new MacroRegistry-based system.
 * Reuses rendering logic from MacroBrowser for consistency and DRY.
 */

import { AutoCompleteOption } from './AutoCompleteOption.js';
import {
    formatMacroSignature,
    createSourceIndicator,
    createAliasIndicator,
    renderMacroDetails,
} from '../macros/MacroBrowser.js';
import { enumIcons } from '../slash-commands/SlashCommandCommonEnumsProvider.js';

/** @typedef {import('../macros/engine/MacroRegistry.js').MacroDefinition} MacroDefinition */

/**
 * Macro context passed from the parser to provide cursor position info.
 * @typedef {Object} MacroAutoCompleteContext
 * @property {string} fullText - The full macro text being typed (without {{ }}).
 * @property {number} cursorOffset - Cursor position within the macro text.
 * @property {string} identifier - The macro identifier (name).
 * @property {string[]} args - Array of arguments typed so far.
 * @property {number} currentArgIndex - Index of the argument being typed (-1 if on identifier).
 */

export class EnhancedMacroAutoCompleteOption extends AutoCompleteOption {
    /** @type {MacroDefinition} */
    #macro;

    /** @type {MacroAutoCompleteContext|null} */
    #context = null;

    /**
     * @param {MacroDefinition} macro - The macro definition from MacroRegistry.
     * @param {MacroAutoCompleteContext} [context] - Optional context for argument hints.
     */
    constructor(macro, context = null) {
        // Use the macro name as the autocomplete key
        super(macro.name, enumIcons.macro);
        this.#macro = macro;
        this.#context = context;
        // nameOffset = 2 to skip the {{ prefix in the display (formatMacroSignature includes braces)
        this.nameOffset = 2;
    }

    /** @returns {MacroDefinition} */
    get macro() {
        return this.#macro;
    }

    /**
     * Renders the list item for the autocomplete dropdown.
     * Tight display: [icon] [signature] [description] [alias icon?] [source icon]
     * @returns {HTMLElement}
     */
    renderItem() {
        const li = document.createElement('li');
        li.classList.add('item', 'macro-ac-item');
        li.setAttribute('data-name', this.name);
        li.setAttribute('data-option-type', 'macro');

        // Type icon
        const type = document.createElement('span');
        type.classList.add('type', 'monospace');
        type.textContent = '{}';
        li.append(type);

        // Specs container (for fuzzy highlight compatibility)
        const specs = document.createElement('span');
        specs.classList.add('specs');

        // Name with character spans for fuzzy highlighting
        const nameEl = document.createElement('span');
        nameEl.classList.add('name', 'monospace');

        // Build signature with individual character spans (includes {{ }})
        const sigText = formatMacroSignature(this.#macro);
        for (const char of sigText) {
            const span = document.createElement('span');
            span.textContent = char;
            nameEl.append(span);
        }
        specs.append(nameEl);
        li.append(specs);

        // Stopgap (spacer for flex layout)
        const stopgap = document.createElement('span');
        stopgap.classList.add('stopgap');
        li.append(stopgap);

        // Help text (description)
        const help = document.createElement('span');
        help.classList.add('help');
        const content = document.createElement('span');
        content.classList.add('helpContent');
        content.textContent = this.#macro.description || '';
        help.append(content);
        li.append(help);

        // Alias indicator icon (if this is an alias)
        const aliasIcon = createAliasIndicator(this.#macro);
        if (aliasIcon) {
            aliasIcon.classList.add('macro-ac-indicator');
            li.append(aliasIcon);
        }

        // Source indicator icon
        const sourceIcon = createSourceIndicator(this.#macro);
        sourceIcon.classList.add('macro-ac-indicator');
        li.append(sourceIcon);

        return li;
    }

    /**
     * Renders the details panel content.
     * Reuses renderMacroDetails from MacroBrowser with autocomplete-specific options.
     * @returns {DocumentFragment}
     */
    renderDetails() {
        const frag = document.createDocumentFragment();

        // Determine current argument index for highlighting
        const currentArgIndex = this.#context?.currentArgIndex ?? -1;

        // Render argument hint banner if we're typing an argument
        if (currentArgIndex >= 0) {
            const hint = this.#renderArgumentHint();
            if (hint) frag.append(hint);
        }

        // Reuse MacroBrowser's renderMacroDetails with options
        const details = renderMacroDetails(this.#macro, { currentArgIndex });

        // Add class for autocomplete-specific styling overrides
        details.classList.add('macro-ac-details');
        frag.append(details);

        return frag;
    }

    /**
     * Renders the current argument hint banner.
     * @returns {HTMLElement|null}
     */
    #renderArgumentHint() {
        if (!this.#context || this.#context.currentArgIndex < 0) return null;

        const argIndex = this.#context.currentArgIndex;
        const isListArg = argIndex >= this.#macro.maxArgs;

        // If we're beyond unnamed args and there's no list, no hint
        if (isListArg && !this.#macro.list) return null;

        const hint = document.createElement('div');
        hint.classList.add('macro-ac-arg-hint');

        const icon = document.createElement('i');
        icon.classList.add('fa-solid', 'fa-arrow-right');
        hint.append(icon);

        if (isListArg) {
            // List argument hint
            const listIndex = argIndex - this.#macro.maxArgs + 1;
            const text = document.createElement('span');
            text.innerHTML = `<strong>List item ${listIndex}</strong>`;
            hint.append(text);
        } else {
            // Unnamed argument hint (required or optional)
            const argDef = this.#macro.unnamedArgDefs[argIndex];
            let optionalLabel = '';
            if (argDef?.optional) {
                optionalLabel = argDef.defaultValue !== undefined
                    ? ` <em>(optional, default: ${argDef.defaultValue === '' ? '<empty string>' : argDef.defaultValue})</em>`
                    : ' <em>(optional)</em>';
            }
            const text = document.createElement('span');
            text.innerHTML = `<strong>${argDef?.name || `Argument ${argIndex + 1}`}</strong>${optionalLabel}`;
            if (argDef?.type) {
                const typeSpan = document.createElement('code');
                typeSpan.classList.add('macro-ac-hint-type');
                if (Array.isArray(argDef.type)) {
                    typeSpan.textContent = argDef.type.join(' | ');
                    typeSpan.title = `Accepts: ${argDef.type.join(', ')}`;
                } else {
                    typeSpan.textContent = argDef.type;
                }
                text.append(' ', typeSpan);
            }
            hint.append(text);

            if (argDef?.description) {
                const descSpan = document.createElement('span');
                descSpan.classList.add('macro-ac-hint-desc');
                descSpan.textContent = ` — ${argDef.description}`;
                hint.append(descSpan);
            }

            if (argDef?.sampleValue) {
                const sampleSpan = document.createElement('span');
                sampleSpan.classList.add('macro-ac-hint-sample');
                sampleSpan.textContent = ` (e.g. ${argDef.sampleValue})`;
                hint.append(sampleSpan);
            }
        }

        return hint;
    }
}

/**
 * Parses the macro text to determine current argument context.
 * @param {string} macroText - The text inside {{ }}, e.g., "roll::1d20" or "random::a::b".
 * @param {number} cursorOffset - Cursor position within macroText.
 * @returns {MacroAutoCompleteContext}
 */
export function parseMacroContext(macroText, cursorOffset) {
    const parts = [];
    let currentPart = '';
    let partStart = 0;
    let i = 0;

    while (i < macroText.length) {
        if (macroText[i] === ':' && macroText[i + 1] === ':') {
            parts.push({ text: currentPart, start: partStart, end: i });
            currentPart = '';
            i += 2;
            partStart = i;
        } else {
            currentPart += macroText[i];
            i++;
        }
    }
    // Push the last part
    parts.push({ text: currentPart, start: partStart, end: macroText.length });

    // Determine which part the cursor is in
    let currentArgIndex = -1;
    for (let idx = 0; idx < parts.length; idx++) {
        const part = parts[idx];
        if (cursorOffset >= part.start && cursorOffset <= part.end) {
            currentArgIndex = idx - 1; // -1 because first part is identifier
            break;
        }
    }

    // If cursor is after all parts (at the end), we're in the last arg
    if (currentArgIndex === -1 && cursorOffset >= parts[parts.length - 1].end) {
        currentArgIndex = parts.length - 1;
    }

    return {
        fullText: macroText,
        cursorOffset,
        identifier: parts[0]?.text.trim() || '',
        args: parts.slice(1).map(p => p.text),
        currentArgIndex,
    };
}