diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 9326f1725..9a0d9696b 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -2608,7 +2608,13 @@ async function executeSlashCommands(text, handleParserErrors = true, scope = nul * @param {Boolean} isFloating Whether to show the auto complete as a floating window (e.g., large QR editor) */ export async function setSlashCommandAutoComplete(textarea, isFloating = false) { - const ac = new SlashCommandAutoComplete(textarea, isFloating); + const parser = new SlashCommandParser(); + const ac = new SlashCommandAutoComplete( + textarea, + () => ac.text[0] == '/', + async(text, index) => await parser.getNameAt(text, index), + isFloating, + ); } /**@type {HTMLTextAreaElement} */ const sendTextarea = document.querySelector('#send_textarea'); diff --git a/public/scripts/slash-commands/SlashCommandAutoComplete.js b/public/scripts/slash-commands/SlashCommandAutoComplete.js index 2550c0cc8..564280484 100644 --- a/public/scripts/slash-commands/SlashCommandAutoComplete.js +++ b/public/scripts/slash-commands/SlashCommandAutoComplete.js @@ -1,15 +1,14 @@ import { power_user } from '../power-user.js'; import { debounce, escapeRegex } from '../utils.js'; import { OPTION_TYPE, SlashCommandAutoCompleteOption, SlashCommandFuzzyScore } from './SlashCommandAutoCompleteOption.js'; -import { SlashCommandParser } from './SlashCommandParser.js'; // eslint-disable-next-line no-unused-vars -import { NAME_RESULT_TYPE, SlashCommandParserNameResult } from './SlashCommandParserNameResult.js'; +import { SlashCommandParserNameResult } from './SlashCommandParserNameResult.js'; export class SlashCommandAutoComplete { /**@type {HTMLTextAreaElement}*/ textarea; /**@type {boolean}*/ isFloating = false; - - /**@type {SlashCommandParser}*/ parser; + /**@type {()=>boolean}*/ checkIfActivate; + /**@type {(text:string, index:number) => Promise}*/ getNameAt; /**@type {boolean}*/ isActive = false; /**@type {boolean}*/ isReplaceable = false; @@ -17,7 +16,7 @@ export class SlashCommandAutoComplete { /**@type {string}*/ text; /**@type {SlashCommandParserNameResult}*/ parserResult; - /**@type {string}*/ slashCommand; + /**@type {string}*/ name; /**@type {boolean}*/ startQuote; /**@type {boolean}*/ endQuote; @@ -55,14 +54,16 @@ export class SlashCommandAutoComplete { /** * @param {HTMLTextAreaElement} textarea The textarea to receive autocomplete. + * @param {() => boolean} checkIfActivate + * @param {(text: string, index: number) => Promise} getNameAt * @param {boolean} isFloating Whether autocomplete should float at the keyboard cursor. */ - constructor(textarea, isFloating = false) { + constructor(textarea, checkIfActivate, getNameAt, isFloating = false) { this.textarea = textarea; + this.checkIfActivate = checkIfActivate; + this.getNameAt = getNameAt; this.isFloating = isFloating; - this.parser = new SlashCommandParser(); - this.domWrap = document.createElement('div'); { this.domWrap.classList.add('slashCommandAutoComplete-wrap'); if (isFloating) this.domWrap.classList.add('isFloating'); @@ -97,21 +98,6 @@ export class SlashCommandAutoComplete { window.addEventListener('resize', ()=>this.updatePositionDebounced()); } - - /** - * Build a cache of DOM list items for autocomplete of slash commands. - */ - buildCache() { - if (!this.hasCache) { - this.hasCache = true; - // init by appending all command options - Object.keys(this.parser.commands).forEach(key=>{ - const cmd = this.parser.commands[key]; - this.items[key] = this.makeItem(new SlashCommandAutoCompleteOption(OPTION_TYPE.COMMAND, cmd, key)); - }); - } - } - /** * * @param {SlashCommandAutoCompleteOption} option @@ -154,7 +140,7 @@ export class SlashCommandAutoComplete { break; } case 'includes': { - const start = item.name.toLowerCase().search(this.slashCommand); + const start = item.name.toLowerCase().search(this.name); chars.forEach((it, idx)=>{ if (idx < start) { it.classList.remove('matched'); @@ -248,129 +234,79 @@ export class SlashCommandAutoComplete { this.text = this.textarea.value; // only show with textarea in focus if (document.activeElement != this.textarea) return this.hide(); - // only show for slash commands - //TODO activation-requirements could be provided as a function - if (this.text[0] != '/') return this.hide(); + // only show if provider wants to + if (!this.checkIfActivate()) return this.hide(); - this.buildCache(); - - // request parser to get command executor (potentially "incomplete", i.e. not an actual existing command) for + // request provider to get name result (potentially "incomplete", i.e. not an actual existing name) for // cursor position - //TODO nameProvider function provided when instantiating? - this.parserResult = this.parser.getNameAt(this.text, this.textarea.selectionStart); - - //TODO options could be fully provided by the name source - switch (this.parserResult?.type) { - case NAME_RESULT_TYPE.CLOSURE: { - this.startQuote = this.text[this.parserResult.start - 2] == '"'; - this.endQuote = this.startQuote && this.text[this.parserResult.start - 2 + this.parserResult.name.length + 1] == '"'; - try { - const qrApi = (await import('../extensions/quick-reply/index.js')).quickReplyApi; - this.parserResult.optionList.push(...qrApi.listSets() - .map(set=>qrApi.listQuickReplies(set).map(qr=>`${set}.${qr}`)) - .flat() - .map(qr=>new SlashCommandAutoCompleteOption(OPTION_TYPE.QUICK_REPLY, qr, qr)), - ); - } catch { /* empty */ } - break; - } - case NAME_RESULT_TYPE.COMMAND: { - this.parserResult.optionList.push(...Object.keys(this.parser.commands) - .map(key=>new SlashCommandAutoCompleteOption(OPTION_TYPE.COMMAND, this.parser.commands[key], key)), - ); - break; - } - default: { - // no result - break; - } - } - this.slashCommand = this.parserResult?.name?.toLowerCase() ?? ''; - // do autocomplete if triggered by a user input and we either don't have an executor or the cursor is at the end - // of the name part of the command - //TODO whether the input is quotable could be an option (given by the parserResult?) - switch (this.parserResult?.type) { - case NAME_RESULT_TYPE.CLOSURE: { - this.isReplaceable = isInput && (!this.parserResult ? true : this.textarea.selectionStart == this.parserResult.start - 2 + this.parserResult.name.length + (this.startQuote ? 1 : 0)); - break; - } - default: // no result - case NAME_RESULT_TYPE.COMMAND: { - this.isReplaceable = isInput && (!this.parserResult ? true : this.textarea.selectionStart == this.parserResult.start - 2 + this.parserResult.name.length); - break; - } - } - - // if [forced (ctrl+space) or user input] and cursor is in the middle of the name part (not at the end) - if (isForced || isInput) { - //TODO input quotable (see above) - switch (this.parserResult?.type) { - case NAME_RESULT_TYPE.CLOSURE: { - if (this.textarea.selectionStart >= this.parserResult.start - 2 && this.textarea.selectionStart <= this.parserResult.start - 2 + this.parserResult.name.length + (this.startQuote ? 1 : 0)) { - this.slashCommand = this.slashCommand.slice(0, this.textarea.selectionStart - (this.parserResult.start - 2) - (this.startQuote ? 1 : 0)); - this.parserResult.name = this.slashCommand; - this.isReplaceable = true; - } - break; - } - case NAME_RESULT_TYPE.COMMAND: { - if (this.textarea.selectionStart >= this.parserResult.start - 2 && this.textarea.selectionStart <= this.parserResult.start - 2 + this.parserResult.name.length) { - this.slashCommand = this.slashCommand.slice(0, this.textarea.selectionStart - (this.parserResult.start - 2)); - this.parserResult.name = this.slashCommand; - this.isReplaceable = true; - } - break; - } - default: { - // no result - break; - } - } - } - - this.fuzzyRegex = new RegExp(`^(.*?)${this.slashCommand.split('').map(char=>`(${escapeRegex(char)})`).join('(.*?)')}(.*?)$`, 'i'); - const matchers = { - 'strict': (name) => name.toLowerCase().startsWith(this.slashCommand), - 'includes': (name) => name.toLowerCase().includes(this.slashCommand), - 'fuzzy': (name) => this.fuzzyRegex.test(name), - }; + this.parserResult = await this.getNameAt(this.text, this.textarea.selectionStart); // don't show if no executor found, i.e. cursor's area is not a command if (!this.parserResult) return this.hide(); - else { - let matchingOptions = this.parserResult.optionList - .filter(it => this.isReplaceable || it.name == '' ? matchers[this.matchType](it.name) : it.name.toLowerCase() == this.slashCommand) // Filter by the input - .filter((it,idx,list) => list.findIndex(opt=>opt.value == it.value) == idx) - ; - this.result = matchingOptions - .filter((it,idx) => matchingOptions.indexOf(it) == idx) - .map(option => { - let li; - //TODO makeItem should be handled in the option class - switch (option.type) { - case OPTION_TYPE.QUICK_REPLY: { - li = this.makeItem(option); - break; - } - case OPTION_TYPE.VARIABLE_NAME: { - li = this.makeItem(option); - break; - } - case OPTION_TYPE.COMMAND: { - li = this.items[option.name]; - break; - } - } - option.replacer = option.name.includes(' ') || this.startQuote || this.endQuote ? `"${option.name}"` : `${option.name}`; - option.dom = li; - if (this.matchType == 'fuzzy') this.fuzzyScore(option); - this.updateName(option); - return option; - }) // Map to the help string and score - .toSorted(this.matchType == 'fuzzy' ? this.fuzzyScoreCompare : (a, b) => a.name.localeCompare(b.name)) // sort by score (if fuzzy) or name - ; + + // need to know if name *can* be inside quotes, and then check if quotes are already there + if (this.parserResult.canBeQuoted) { + this.startQuote = this.text[this.parserResult.start] == '"'; + this.endQuote = this.startQuote && this.text[this.parserResult.start + this.parserResult.name.length + 1] == '"'; + } else { + this.startQuote = false; + this.endQuote = false; } + // use lowercase name for matching + this.name = this.parserResult?.name?.toLowerCase() ?? ''; + + // do autocomplete if triggered by a user input and we either don't have an executor or the cursor is at the end + // of the name part of the command + this.isReplaceable = isInput && (!this.parserResult ? true : this.textarea.selectionStart == this.parserResult.start + this.parserResult.name.length + (this.startQuote ? 1 : 0)); + + // if [forced (ctrl+space) or user input] and cursor is in the middle of the name part (not at the end) + if (isForced || isInput) { + if (this.textarea.selectionStart >= this.parserResult.start && this.textarea.selectionStart <= this.parserResult.start + this.parserResult.name.length + (this.startQuote ? 1 : 0)) { + this.name = this.name.slice(0, this.textarea.selectionStart - (this.parserResult.start) - (this.startQuote ? 1 : 0)); + this.parserResult.name = this.name; + this.isReplaceable = true; + } + } + + // only build the fuzzy regex if match type is set to fuzzy + if (this.matchType == 'fuzzy') { + this.fuzzyRegex = new RegExp(`^(.*?)${this.name.split('').map(char=>`(${escapeRegex(char)})`).join('(.*?)')}(.*?)$`, 'i'); + } + + //TODO maybe move the matchers somewhere else; a single match function? matchType is available as property + const matchers = { + 'strict': (name) => name.toLowerCase().startsWith(this.name), + 'includes': (name) => name.toLowerCase().includes(this.name), + 'fuzzy': (name) => this.fuzzyRegex.test(name), + }; + + this.result = this.parserResult.optionList + // filter the list of options by the partial name according to the matching type + .filter(it => this.isReplaceable || it.name == '' ? matchers[this.matchType](it.name) : it.name.toLowerCase() == this.name) + // remove aliases + .filter((it,idx,list) => list.findIndex(opt=>opt.value == it.value) == idx) + // update remaining options + .map(option => { + // build element + option.dom = this.makeItem(option); + // update replacer and add quotes if necessary + if (this.parserResult.canBeQuoted) { + option.replacer = option.name.includes(' ') || this.startQuote || this.endQuote ? `"${option.name}"` : `${option.name}`; + } else { + option.replacer = option.name; + } + // calculate fuzzy score if matching is fuzzy + if (this.matchType == 'fuzzy') this.fuzzyScore(option); + // update the name to highlight the matched chars + this.updateName(option); + return option; + }) + // sort by fuzzy score or alphabetical + .toSorted(this.matchType == 'fuzzy' ? this.fuzzyScoreCompare : (a, b) => a.name.localeCompare(b.name)) + ; + + if (this.result.length == 0) { // no result and no input? hide autocomplete if (!isInput) { @@ -382,27 +318,13 @@ export class SlashCommandAutoComplete { null, '', ); - switch (this.parserResult?.type) { - //TODO "no-match" text should be an option (in parserResult?) - case NAME_RESULT_TYPE.CLOSURE: { - const li = document.createElement('li'); { - li.textContent = this.slashCommand.length ? - `No matching variables in scope and no matching Quick Replies for "${this.slashCommand}"` - : 'No variables in scope and no Quick Replies found.'; - } - option.dom = li; - this.result.push(option); - break; - } - case NAME_RESULT_TYPE.COMMAND: { - const li = document.createElement('li'); { - li.textContent = `No matching commands for "/${this.slashCommand}"`; - } - option.dom = li; - this.result.push(option); - break; - } + const li = document.createElement('li'); { + li.textContent = this.name.length ? + this.parserResult.makeNoMatchText() + : this.parserResult.makeNoOptionstext(); } + option.dom = li; + this.result.push(option); } else if (this.result.length == 1 && this.parserResult && this.result[0].name == this.parserResult.name) { // only one result that is exactly the current value? just show hint, no autocomplete this.isReplaceable = false; @@ -637,10 +559,10 @@ export class SlashCommandAutoComplete { */ async select() { if (this.isReplaceable && this.selectedItem.value !== null) { - this.textarea.value = `${this.text.slice(0, this.parserResult.start - 2)}${this.selectedItem.replacer}${this.text.slice(this.parserResult.start - 2 + this.parserResult.name.length + (this.startQuote ? 1 : 0) + (this.endQuote ? 1 : 0))}`; + this.textarea.value = `${this.text.slice(0, this.parserResult.start)}${this.selectedItem.replacer}${this.text.slice(this.parserResult.start + this.parserResult.name.length + (this.startQuote ? 1 : 0) + (this.endQuote ? 1 : 0))}`; await this.pointerup; this.textarea.focus(); - this.textarea.selectionStart = this.parserResult.start - 2 + this.selectedItem.replacer.length; + this.textarea.selectionStart = this.parserResult.start + this.selectedItem.replacer.length; this.textarea.selectionEnd = this.textarea.selectionStart; this.show(); } else { @@ -704,7 +626,7 @@ export class SlashCommandAutoComplete { case 'Enter': { // pick the selected item to autocomplete if (evt.ctrlKey || evt.altKey || evt.shiftKey || this.selectedItem.type == OPTION_TYPE.BLANK) break; - if (this.selectedItem.name == this.slashCommand) break; + if (this.selectedItem.name == this.name) break; evt.preventDefault(); evt.stopImmediatePropagation(); this.select(); diff --git a/public/scripts/slash-commands/SlashCommandAutoCompleteOption.js b/public/scripts/slash-commands/SlashCommandAutoCompleteOption.js index 996ab2630..58b023f13 100644 --- a/public/scripts/slash-commands/SlashCommandAutoCompleteOption.js +++ b/public/scripts/slash-commands/SlashCommandAutoCompleteOption.js @@ -2,15 +2,6 @@ import { SlashCommand } from './SlashCommand.js'; -/**@readonly*/ -/**@enum {Number}*/ -export const OPTION_TYPE = { - 'COMMAND': 1, - 'QUICK_REPLY': 2, - 'VARIABLE_NAME': 3, - 'BLANK': 4, -}; - export class SlashCommandFuzzyScore { /**@type {number}*/ start; /**@type {number}*/ longestConsecutive; @@ -27,7 +18,6 @@ export class SlashCommandFuzzyScore { export class SlashCommandAutoCompleteOption { - /**@type {OPTION_TYPE}*/ type; /**@type {string|SlashCommand}*/ value; /**@type {string}*/ name; /**@type {SlashCommandFuzzyScore}*/ score; @@ -36,39 +26,15 @@ export class SlashCommandAutoCompleteOption { /** - * @param {OPTION_TYPE} type * @param {string|SlashCommand} value * @param {string} name */ - constructor(type, value, name) { - this.type = type; + constructor(value, name) { this.value = value; this.name = name; } - renderItem() { - let li; - switch (this.type) { - case OPTION_TYPE.COMMAND: { - /**@type {SlashCommand}*/ - // @ts-ignore - const cmd = this.value; - li = cmd.renderHelpItem(this.name); - break; - } - case OPTION_TYPE.QUICK_REPLY: { - li = this.makeItem(this.name, 'QR', true); - break; - } - case OPTION_TYPE.VARIABLE_NAME: { - li = this.makeItem(this.name, '𝑥', true); - break; - } - } - li.setAttribute('data-name', this.name); - return li; - } makeItem(key, typeIcon, noSlash, namedArguments = [], unnamedArguments = [], returnType = 'void', helpString = '', aliasList = []) { const li = document.createElement('li'); { li.classList.add('item'); @@ -174,7 +140,7 @@ export class SlashCommandAutoCompleteOption { const returns = document.createElement('span'); { returns.classList.add('returns'); returns.textContent = returnType ?? 'void'; - body.append(returns); + // body.append(returns); } specs.append(body); } @@ -207,85 +173,17 @@ export class SlashCommandAutoCompleteOption { // li.append(aliases); } } - // gotta listen to pointerdown (happens before textarea-blur) - li.addEventListener('pointerdown', ()=>{ - // gotta catch pointerup to restore focus to textarea (blurs after pointerdown) - this.pointerup = new Promise(resolve=>{ - const resolver = ()=>{ - window.removeEventListener('pointerup', resolver); - resolve(); - }; - window.addEventListener('pointerup', resolver); - }); - this.select(); - }); } return li; } + renderItem() { + throw new Error(`${this.constructor.name}.renderItem() is not implemented`); + } + + renderDetails() { - switch (this.type) { - case OPTION_TYPE.COMMAND: { - return this.renderCommandDetails(); - } - case OPTION_TYPE.QUICK_REPLY: { - return this.renderQuickReplyDetails(); - } - case OPTION_TYPE.VARIABLE_NAME: { - return this.renderVariableDetails(); - } - default: { - return this.renderBlankDetails(); - } - } - } - renderBlankDetails() { - return 'BLANK'; - } - renderQuickReplyDetails() { - const frag = document.createDocumentFragment(); - const specs = document.createElement('div'); { - specs.classList.add('specs'); - const name = document.createElement('div'); { - name.classList.add('name'); - name.classList.add('monospace'); - name.textContent = this.value.toString(); - specs.append(name); - } - frag.append(specs); - } - const help = document.createElement('span'); { - help.classList.add('help'); - help.textContent = 'Quick Reply'; - frag.append(help); - } - return frag; - } - renderVariableDetails() { - const frag = document.createDocumentFragment(); - const specs = document.createElement('div'); { - specs.classList.add('specs'); - const name = document.createElement('div'); { - name.classList.add('name'); - name.classList.add('monospace'); - name.textContent = this.value.toString(); - specs.append(name); - } - frag.append(specs); - } - const help = document.createElement('span'); { - help.classList.add('help'); - help.textContent = 'scoped variable'; - frag.append(help); - } - return frag; - } - renderCommandDetails() { - const key = this.name; - /**@type {SlashCommand} */ - // @ts-ignore - const cmd = this.value; - return cmd.renderHelpDetails(key); + throw new Error(`${this.constructor.name}.renderDetails() is not implemented`); } } diff --git a/public/scripts/slash-commands/SlashCommandCommandAutoCompleteOption.js b/public/scripts/slash-commands/SlashCommandCommandAutoCompleteOption.js new file mode 100644 index 000000000..e7d38d7a4 --- /dev/null +++ b/public/scripts/slash-commands/SlashCommandCommandAutoCompleteOption.js @@ -0,0 +1,30 @@ +import { SlashCommand } from './SlashCommand.js'; +import { SlashCommandAutoCompleteOption } from './SlashCommandAutoCompleteOption.js'; + +export class SlashCommandCommandAutoCompleteOption extends SlashCommandAutoCompleteOption { + /**@type {SlashCommand} */ + get cmd() { + // @ts-ignore + return this.value; + } + /** + * @param {SlashCommand} value + * @param {string} name + */ + constructor(value, name) { + super(value, name); + } + + + renderItem() { + let li; + li = this.cmd.renderHelpItem(this.name); + li.setAttribute('data-name', this.name); + return li; + } + + + renderDetails() { + return this.cmd.renderHelpDetails(this.name); + } +} diff --git a/public/scripts/slash-commands/SlashCommandParser.js b/public/scripts/slash-commands/SlashCommandParser.js index 2e3adf0c2..655e143ef 100644 --- a/public/scripts/slash-commands/SlashCommandParser.js +++ b/public/scripts/slash-commands/SlashCommandParser.js @@ -1,14 +1,16 @@ import { power_user } from '../power-user.js'; import { isTrueBoolean, uuidv4 } from '../utils.js'; import { SlashCommand } from './SlashCommand.js'; -import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './SlashCommandArgument.js'; -import { OPTION_TYPE, SlashCommandAutoCompleteOption } from './SlashCommandAutoCompleteOption.js'; +import { ARGUMENT_TYPE, SlashCommandArgument } from './SlashCommandArgument.js'; import { SlashCommandClosure } from './SlashCommandClosure.js'; +import { SlashCommandCommandAutoCompleteOption } from './SlashCommandCommandAutoCompleteOption.js'; import { SlashCommandExecutor } from './SlashCommandExecutor.js'; import { SlashCommandParserError } from './SlashCommandParserError.js'; import { NAME_RESULT_TYPE, SlashCommandParserNameResult } from './SlashCommandParserNameResult.js'; +import { SlashCommandQuickReplyAutoCompleteOption } from './SlashCommandQuickReplyAutoCompleteOption.js'; // eslint-disable-next-line no-unused-vars import { SlashCommandScope } from './SlashCommandScope.js'; +import { SlashCommandVariableAutoCompleteOption } from './SlashCommandVariableAutoCompleteOption.js'; /**@readonly*/ /**@enum {Number}*/ @@ -345,7 +347,7 @@ export class SlashCommandParser { * @param {*} text The text to parse. * @param {*} index Index to check for names (cursor position). */ - getNameAt(text, index) { + async getNameAt(text, index) { if (this.text != `{:${text}:}`) { try { this.parse(text, false); @@ -368,22 +370,43 @@ export class SlashCommandParser { ; if (childClosure !== null) return null; if (executor.name == ':') { - return new SlashCommandParserNameResult( + const options = this.scopeIndex[this.commandIndex.indexOf(executor)] + ?.allVariableNames + ?.map(it=>new SlashCommandVariableAutoCompleteOption(it)) + ?? [] + ; + try { + const qrApi = (await import('../extensions/quick-reply/index.js')).quickReplyApi; + options.push(...qrApi.listSets() + .map(set=>qrApi.listQuickReplies(set).map(qr=>`${set}.${qr}`)) + .flat() + .map(qr=>new SlashCommandQuickReplyAutoCompleteOption(qr)), + ); + } catch { /* empty */ } + const result = new SlashCommandParserNameResult( NAME_RESULT_TYPE.CLOSURE, executor.value.toString(), - executor.start, - this.scopeIndex[this.commandIndex.indexOf(executor)] - ?.allVariableNames - ?.map(it=>new SlashCommandAutoCompleteOption(OPTION_TYPE.VARIABLE_NAME, it, it)) - ?? [] - , + executor.start - 2, + options, + true, + ()=>`No matching variables in scope and no matching Quick Replies for "${result.name}"`, + ()=>'No variables in scope and no Quick Replies found.', ); + return result; } - return new SlashCommandParserNameResult( + const result = new SlashCommandParserNameResult( NAME_RESULT_TYPE.COMMAND, executor.name, - executor.start, + executor.start - 2, + Object + .keys(this.commands) + .map(key=>new SlashCommandCommandAutoCompleteOption(this.commands[key], key)) + , + false, + ()=>`No matching slash commands for "/${result.name}"`, + ()=>'No slash commands found!', ); + return result; } return null; } diff --git a/public/scripts/slash-commands/SlashCommandParserNameResult.js b/public/scripts/slash-commands/SlashCommandParserNameResult.js index 0c11421be..b996025eb 100644 --- a/public/scripts/slash-commands/SlashCommandParserNameResult.js +++ b/public/scripts/slash-commands/SlashCommandParserNameResult.js @@ -15,6 +15,9 @@ export class SlashCommandParserNameResult { /**@type {string} */ name; /**@type {number} */ start; /**@type {SlashCommandAutoCompleteOption[]} */ optionList = []; + /**@type {boolean} */ canBeQuoted = false; + /**@type {()=>string} */ makeNoMatchText = ()=>`No matches found for "${this.name}"`; + /**@type {()=>string} */ makeNoOptionstext = ()=>'No options'; /** @@ -22,11 +25,17 @@ export class SlashCommandParserNameResult { * @param {string} name Name (potentially partial) of the name at the requested index. * @param {number} start Index where the name starts. * @param {SlashCommandAutoCompleteOption[]} optionList A list of autocomplete options found in the current scope. + * @param {boolean} canBeQuoted Whether the name can be inside quotes. + * @param {()=>string} makeNoMatchText Function that returns text to show when no matches where found. + * @param {()=>string} makeNoOptionsText Function that returns text to show when no options are available to match against. */ - constructor(type, name, start, optionList = []) { + constructor(type, name, start, optionList = [], canBeQuoted = false, makeNoMatchText = null, makeNoOptionsText = null) { this.type = type; this.name = name; this.start = start; this.optionList = optionList; + this.canBeQuoted = canBeQuoted; + this.noMatchText = makeNoMatchText ?? this.makeNoMatchText; + this.noOptionstext = makeNoOptionsText ?? this.makeNoOptionstext; } } diff --git a/public/scripts/slash-commands/SlashCommandQuickReplyAutoCompleteOption.js b/public/scripts/slash-commands/SlashCommandQuickReplyAutoCompleteOption.js new file mode 100644 index 000000000..8b18dbdb2 --- /dev/null +++ b/public/scripts/slash-commands/SlashCommandQuickReplyAutoCompleteOption.js @@ -0,0 +1,39 @@ +import { SlashCommandAutoCompleteOption } from './SlashCommandAutoCompleteOption.js'; + +export class SlashCommandQuickReplyAutoCompleteOption extends SlashCommandAutoCompleteOption { + /** + * @param {string} value + */ + constructor(value) { + super(value, value); + } + + + renderItem() { + let li; + li = this.makeItem(this.name, 'QR', true); + li.setAttribute('data-name', this.name); + return li; + } + + + renderDetails() { + const frag = document.createDocumentFragment(); + const specs = document.createElement('div'); { + specs.classList.add('specs'); + const name = document.createElement('div'); { + name.classList.add('name'); + name.classList.add('monospace'); + name.textContent = this.value.toString(); + specs.append(name); + } + frag.append(specs); + } + const help = document.createElement('span'); { + help.classList.add('help'); + help.textContent = 'Quick Reply'; + frag.append(help); + } + return frag; + } +} diff --git a/public/scripts/slash-commands/SlashCommandVariableAutoCompleteOption.js b/public/scripts/slash-commands/SlashCommandVariableAutoCompleteOption.js new file mode 100644 index 000000000..2d258dc47 --- /dev/null +++ b/public/scripts/slash-commands/SlashCommandVariableAutoCompleteOption.js @@ -0,0 +1,39 @@ +import { SlashCommandAutoCompleteOption } from './SlashCommandAutoCompleteOption.js'; + +export class SlashCommandVariableAutoCompleteOption extends SlashCommandAutoCompleteOption { + /** + * @param {string} value + */ + constructor(value) { + super(value, value); + } + + + renderItem() { + let li; + li = this.makeItem(this.name, '𝑥', true); + li.setAttribute('data-name', this.name); + return li; + } + + + renderDetails() { + const frag = document.createDocumentFragment(); + const specs = document.createElement('div'); { + specs.classList.add('specs'); + const name = document.createElement('div'); { + name.classList.add('name'); + name.classList.add('monospace'); + name.textContent = this.value.toString(); + specs.append(name); + } + frag.append(specs); + } + const help = document.createElement('span'); { + help.classList.add('help'); + help.textContent = 'scoped variable'; + frag.append(help); + } + return frag; + } +}