mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
clean up autocomplete
This commit is contained in:
@ -56,6 +56,8 @@ import { background_settings } from './backgrounds.js';
|
|||||||
import { SlashCommandScope } from './slash-commands/SlashCommandScope.js';
|
import { SlashCommandScope } from './slash-commands/SlashCommandScope.js';
|
||||||
import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
|
import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
|
||||||
import { SlashCommandClosureResult } from './slash-commands/SlashCommandClosureResult.js';
|
import { SlashCommandClosureResult } from './slash-commands/SlashCommandClosureResult.js';
|
||||||
|
import { NAME_RESULT_TYPE, SlashCommandParserNameResult } from './slash-commands/SlashCommandParserNameResult.js';
|
||||||
|
import { OPTION_TYPE, SlashCommandAutoCompleteOption } from './slash-commands/SlashCommandAutoCompleteOption.js';
|
||||||
export {
|
export {
|
||||||
executeSlashCommands, getSlashCommandsHelp, registerSlashCommand,
|
executeSlashCommands, getSlashCommandsHelp, registerSlashCommand,
|
||||||
};
|
};
|
||||||
@ -1797,8 +1799,8 @@ export async function setSlashCommandAutoComplete(textarea, isFloating = false)
|
|||||||
let selectedItem = null;
|
let selectedItem = null;
|
||||||
let isActive = false;
|
let isActive = false;
|
||||||
let text;
|
let text;
|
||||||
/**@type {SlashCommandExecutor}*/
|
/**@type {SlashCommandParserNameResult}*/
|
||||||
let executor;
|
let parserResult;
|
||||||
let clone;
|
let clone;
|
||||||
let startQuote;
|
let startQuote;
|
||||||
let endQuote;
|
let endQuote;
|
||||||
@ -1816,56 +1818,73 @@ export async function setSlashCommandAutoComplete(textarea, isFloating = false)
|
|||||||
|
|
||||||
// request parser to get command executor (potentially "incomplete", i.e. not an actual existing command) for
|
// request parser to get command executor (potentially "incomplete", i.e. not an actual existing command) for
|
||||||
// cursor position
|
// cursor position
|
||||||
const parserResult = parser.getCommandAt(text, textarea.selectionStart);
|
parserResult = parser.getNameAt(text, textarea.selectionStart);
|
||||||
let run;
|
switch (parserResult?.type) {
|
||||||
let options;
|
case NAME_RESULT_TYPE.CLOSURE: {
|
||||||
if (Array.isArray(parserResult)) {
|
startQuote = text[parserResult.start - 2] == '"';
|
||||||
executor = null;
|
endQuote = startQuote && text[parserResult.start - 2 + parserResult.name.length + 1] == '"';
|
||||||
run = parserResult[0];
|
|
||||||
options = parserResult[1];
|
|
||||||
startQuote = text[run.start - 2] == '"';
|
|
||||||
endQuote = startQuote && text[run.start - 2 + run.value.length + 1] == '"';
|
|
||||||
try {
|
try {
|
||||||
const qrApi = (await import('./extensions/quick-reply/index.js')).quickReplyApi;
|
const qrApi = (await import('./extensions/quick-reply/index.js')).quickReplyApi;
|
||||||
options.push(...qrApi.listSets().map(set=>qrApi.listQuickReplies(set).map(qr=>`${set}.${qr}`)).flat());
|
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 */ }
|
} catch { /* empty */ }
|
||||||
} else {
|
break;
|
||||||
executor = parserResult;
|
|
||||||
}
|
}
|
||||||
let slashCommand = run ? run.value?.toLowerCase() : executor?.name?.toLowerCase() ?? '';
|
default: // no result -> empty slash "/" -> list all commands
|
||||||
|
case NAME_RESULT_TYPE.COMMAND: {
|
||||||
|
parserResult.optionList.push(...Object.keys(parser.commands)
|
||||||
|
.map(key=>new SlashCommandAutoCompleteOption(OPTION_TYPE.COMMAND, parser.commands[key], key)),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let slashCommand = 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
|
// 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
|
// of the name part of the command
|
||||||
if (options) {
|
switch (parserResult?.type) {
|
||||||
isReplacable = isInput && (!run.value ? true : textarea.selectionStart == run.start - 2 + run.value.length + (startQuote ? 1 : 0));
|
case NAME_RESULT_TYPE.CLOSURE: {
|
||||||
} else {
|
isReplacable = isInput && (!parserResult ? true : textarea.selectionStart == parserResult.start - 2 + parserResult.name.length + (startQuote ? 1 : 0));
|
||||||
isReplacable = isInput && (!executor ? true : textarea.selectionStart == executor.start - 2 + executor.name.length);
|
break;
|
||||||
}
|
}
|
||||||
|
default: // no result -> empty slash "/" -> list all commands
|
||||||
|
case NAME_RESULT_TYPE.COMMAND: {
|
||||||
|
isReplacable = isInput && (!parserResult ? true : textarea.selectionStart == parserResult.start - 2 + 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 forced (ctrl+space) or user input and cursor is in the middle of the name part (not at the end)
|
||||||
if ((isForced || isInput)
|
if (isForced || isInput) {
|
||||||
&& (
|
switch (parserResult?.type) {
|
||||||
((executor) && textarea.selectionStart >= executor.start - 2 && textarea.selectionStart <= executor.start - 2 + executor.name.length)
|
case NAME_RESULT_TYPE.CLOSURE: {
|
||||||
||
|
if (textarea.selectionStart >= parserResult.start - 2 && textarea.selectionStart <= parserResult.start - 2 + parserResult.name.length + (startQuote ? 1 : 0)) {
|
||||||
((run) && textarea.selectionStart >= run.start - 2 && textarea.selectionStart <= run.start - 2 + run.value.length + (startQuote ? 1 : 0))
|
slashCommand = slashCommand.slice(0, textarea.selectionStart - (parserResult.start - 2) - (startQuote ? 1 : 0));
|
||||||
)
|
parserResult.name = slashCommand;
|
||||||
){
|
|
||||||
if (run) {
|
|
||||||
slashCommand = slashCommand.slice(0, textarea.selectionStart - (run.start - 2) - (startQuote ? 1 : 0));
|
|
||||||
run.value = slashCommand;
|
|
||||||
run.end = run.start + slashCommand.length;
|
|
||||||
} else {
|
|
||||||
slashCommand = slashCommand.slice(0, textarea.selectionStart - (executor.start - 2));
|
|
||||||
executor.name = slashCommand;
|
|
||||||
executor.end = executor.start + slashCommand.length;
|
|
||||||
}
|
|
||||||
isReplacable = true;
|
isReplacable = true;
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: // no result -> empty slash "/" -> list all commands
|
||||||
|
case NAME_RESULT_TYPE.COMMAND: {
|
||||||
|
if (textarea.selectionStart >= parserResult.start - 2 && textarea.selectionStart <= parserResult.start - 2 + parserResult.name.length) {
|
||||||
|
slashCommand = slashCommand.slice(0, textarea.selectionStart - (parserResult.start - 2));
|
||||||
|
parserResult.name = slashCommand;
|
||||||
|
isReplacable = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const matchType = power_user.stscript?.matching ?? 'strict';
|
const matchType = power_user.stscript?.matching ?? 'strict';
|
||||||
const fuzzyRegex = new RegExp(`^(.*?)${slashCommand.split('').map(char=>`(${escapeRegex(char)})`).join('(.*?)')}(.*?)$`, 'i');
|
const fuzzyRegex = new RegExp(`^(.*?)${slashCommand.split('').map(char=>`(${escapeRegex(char)})`).join('(.*?)')}(.*?)$`, 'i');
|
||||||
const matchers = {
|
const matchers = {
|
||||||
'strict': (cmd) => cmd.toLowerCase().startsWith(slashCommand),
|
'strict': (name) => name.toLowerCase().startsWith(slashCommand),
|
||||||
'includes': (cmd) => cmd.toLowerCase().includes(slashCommand),
|
'includes': (name) => name.toLowerCase().includes(slashCommand),
|
||||||
'fuzzy': (cmd) => fuzzyRegex.test(cmd),
|
'fuzzy': (name) => fuzzyRegex.test(name),
|
||||||
};
|
};
|
||||||
const fuzzyScore = (name) => {
|
const fuzzyScore = (name) => {
|
||||||
const parts = fuzzyRegex.exec(name).slice(1, -1);
|
const parts = fuzzyRegex.exec(name).slice(1, -1);
|
||||||
@ -1931,39 +1950,50 @@ export async function setSlashCommandAutoComplete(textarea, isFloating = false)
|
|||||||
};
|
};
|
||||||
|
|
||||||
// don't show if no executor found, i.e. cursor's area is not a command
|
// don't show if no executor found, i.e. cursor's area is not a command
|
||||||
if (!executor && !options) return hide();
|
if (!parserResult) return hide();
|
||||||
else {
|
else {
|
||||||
const helpStrings = (options ?? Object.keys(parser.commands)) // Get all slash commands
|
const matchingOptions = parserResult.optionList
|
||||||
.filter(it => run?.value == '' || executor?.name == '' || isReplacable ? matchers[matchType](it) : it.toLowerCase() == slashCommand) // Filter by the input
|
.filter(it => isReplacable || it.name == '' ? matchers[matchType](it.name) : it.name.toLowerCase() == slashCommand) // Filter by the input
|
||||||
;
|
;
|
||||||
result = helpStrings
|
result = matchingOptions
|
||||||
.filter((it,idx)=>options || [idx, -1].includes(helpStrings.indexOf(parser.commands[it].name.toLowerCase()))) // remove duplicates
|
.filter((it,idx) => matchingOptions.indexOf(it) == idx)
|
||||||
.map(name => {
|
.map(option => {
|
||||||
if (options) {
|
let typeIcon = '';
|
||||||
return {
|
let noSlash = false;
|
||||||
name: name,
|
let helpString = '';
|
||||||
label: `<span class="type monospace">${name.includes('.')?'QR':'𝑥'}</span> ${buildHelpStringName(name, true)}`,
|
|
||||||
value: name.includes(' ') || startQuote || endQuote ? `"${name}"` : `${name}`,
|
|
||||||
score: matchType == 'fuzzy' ? fuzzyScore(name) : null,
|
|
||||||
li: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const cmd = parser.commands[name];
|
|
||||||
let aliases = '';
|
let aliases = '';
|
||||||
if (cmd.aliases?.length > 0) {
|
switch (option.type) {
|
||||||
|
case OPTION_TYPE.QUICK_REPLY: {
|
||||||
|
typeIcon = 'QR';
|
||||||
|
noSlash = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case OPTION_TYPE.VARIABLE_NAME: {
|
||||||
|
typeIcon = '𝑥';
|
||||||
|
noSlash = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case OPTION_TYPE.COMMAND: {
|
||||||
|
typeIcon = '/';
|
||||||
|
noSlash = false;
|
||||||
|
helpString = option.value.helpString;
|
||||||
|
if (option.value.aliases.length > 0) {
|
||||||
aliases = ' (alias: ';
|
aliases = ' (alias: ';
|
||||||
aliases += [cmd.name, ...cmd.aliases]
|
aliases += [option.value.name, ...option.value.aliases]
|
||||||
.filter(it=>it != name)
|
.filter(it=>it != option)
|
||||||
.map(it=>`<span class="monospace">/${it}</span>`)
|
.map(it=>`<span class="monospace">/${it}</span>`)
|
||||||
.join(', ')
|
.join(', ')
|
||||||
;
|
;
|
||||||
aliases += ')';
|
aliases += ')';
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
name: name,
|
name: option.name,
|
||||||
label: `${buildHelpStringName(name)}${cmd.helpString}${aliases}`,
|
label: `<span class="type monospace">${typeIcon}</span> ${buildHelpStringName(option.name, noSlash)}${helpString}${aliases}`,
|
||||||
value: `${name}`,
|
value: option.name.includes(' ') || startQuote || endQuote ? `"${option.name}"` : `${option.name}`,
|
||||||
score: matchType == 'fuzzy' ? fuzzyScore(name) : null,
|
score: matchType == 'fuzzy' ? fuzzyScore(option.name) : null,
|
||||||
li: null,
|
li: null,
|
||||||
};
|
};
|
||||||
}) // Map to the help string and score
|
}) // Map to the help string and score
|
||||||
@ -1977,17 +2007,20 @@ export async function setSlashCommandAutoComplete(textarea, isFloating = false)
|
|||||||
return hide();
|
return hide();
|
||||||
}
|
}
|
||||||
// otherwise add "no match" notice
|
// otherwise add "no match" notice
|
||||||
if (options) {
|
switch (parserResult.type) {
|
||||||
|
case NAME_RESULT_TYPE.CLOSURE: {
|
||||||
result.push({
|
result.push({
|
||||||
name: '',
|
name: '',
|
||||||
label: slashCommand.length ?
|
label: slashCommand.length ?
|
||||||
`No matching variables in scope for "${slashCommand}"`
|
`No matching variables in scope and no matching Quick Replies for "${slashCommand}"`
|
||||||
: 'No variables in scope.',
|
: 'No variables in scope and no Quick Replies found.',
|
||||||
value: null,
|
value: null,
|
||||||
score: null,
|
score: null,
|
||||||
li: null,
|
li: null,
|
||||||
});
|
});
|
||||||
} else {
|
break;
|
||||||
|
}
|
||||||
|
case NAME_RESULT_TYPE.COMMAND: {
|
||||||
result.push({
|
result.push({
|
||||||
name: '',
|
name: '',
|
||||||
label: `No matching commands for "/${slashCommand}"`,
|
label: `No matching commands for "/${slashCommand}"`,
|
||||||
@ -1995,8 +2028,10 @@ export async function setSlashCommandAutoComplete(textarea, isFloating = false)
|
|||||||
score: null,
|
score: null,
|
||||||
li: null,
|
li: null,
|
||||||
});
|
});
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
} else if (result.length == 1 && ((executor && result[0].value == `/${executor.name}`) || (options && result[0].value == run.value))) {
|
}
|
||||||
|
} else if (result.length == 1 && parserResult && result[0].name == parserResult.name) {
|
||||||
// only one result that is exactly the current value? just show hint, no autocomplete
|
// only one result that is exactly the current value? just show hint, no autocomplete
|
||||||
isReplacable = false;
|
isReplacable = false;
|
||||||
} else if (!isReplacable && result.length > 1) {
|
} else if (!isReplacable && result.length > 1) {
|
||||||
@ -2051,9 +2086,6 @@ export async function setSlashCommandAutoComplete(textarea, isFloating = false)
|
|||||||
updatePosition();
|
updatePosition();
|
||||||
document.body.append(dom);
|
document.body.append(dom);
|
||||||
isActive = true;
|
isActive = true;
|
||||||
if (options) {
|
|
||||||
executor = {start:run.start, name:`${slashCommand}`};
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
const updatePosition = () => {
|
const updatePosition = () => {
|
||||||
if (isFloating) {
|
if (isFloating) {
|
||||||
@ -2128,10 +2160,10 @@ export async function setSlashCommandAutoComplete(textarea, isFloating = false)
|
|||||||
let pointerup = Promise.resolve();
|
let pointerup = Promise.resolve();
|
||||||
const select = async() => {
|
const select = async() => {
|
||||||
if (isReplacable && selectedItem.value !== null) {
|
if (isReplacable && selectedItem.value !== null) {
|
||||||
textarea.value = `${text.slice(0, executor.start - 2)}${selectedItem.value}${text.slice(executor.start - 2 + executor.name.length + (startQuote ? 1 : 0) + (endQuote ? 1 : 0))}`;
|
textarea.value = `${text.slice(0, parserResult.start - 2)}${selectedItem.value}${text.slice(parserResult.start - 2 + parserResult.name.length + (startQuote ? 1 : 0) + (endQuote ? 1 : 0))}`;
|
||||||
await pointerup;
|
await pointerup;
|
||||||
textarea.focus();
|
textarea.focus();
|
||||||
textarea.selectionStart = executor.start - 2 + selectedItem.value.length;
|
textarea.selectionStart = parserResult.start - 2 + selectedItem.value.length;
|
||||||
textarea.selectionEnd = textarea.selectionStart;
|
textarea.selectionEnd = textarea.selectionStart;
|
||||||
show();
|
show();
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
import { SlashCommand } from './SlashCommand.js';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**@readonly*/
|
||||||
|
/**@enum {Number}*/
|
||||||
|
export const OPTION_TYPE = {
|
||||||
|
'COMMAND': 1,
|
||||||
|
'QUICK_REPLY': 2,
|
||||||
|
'VARIABLE_NAME': 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export class SlashCommandAutoCompleteOption {
|
||||||
|
/**@type {OPTION_TYPE}*/ type;
|
||||||
|
/**@type {string|SlashCommand}*/ value;
|
||||||
|
/**@type {string}*/ name;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {OPTION_TYPE} type
|
||||||
|
* @param {string|SlashCommand} value
|
||||||
|
* @param {string} name
|
||||||
|
*/
|
||||||
|
constructor(type, value, name) {
|
||||||
|
this.type = type;
|
||||||
|
this.value = value;
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,9 @@
|
|||||||
import { SlashCommand } from './SlashCommand.js';
|
import { SlashCommand } from './SlashCommand.js';
|
||||||
|
import { OPTION_TYPE, SlashCommandAutoCompleteOption } from './SlashCommandAutoCompleteOption.js';
|
||||||
import { SlashCommandClosure } from './SlashCommandClosure.js';
|
import { SlashCommandClosure } from './SlashCommandClosure.js';
|
||||||
import { SlashCommandClosureExecutor } from './SlashCommandClosureExecutor.js';
|
|
||||||
import { SlashCommandExecutor } from './SlashCommandExecutor.js';
|
import { SlashCommandExecutor } from './SlashCommandExecutor.js';
|
||||||
import { SlashCommandParserError } from './SlashCommandParserError.js';
|
import { SlashCommandParserError } from './SlashCommandParserError.js';
|
||||||
|
import { NAME_RESULT_TYPE, SlashCommandParserNameResult } from './SlashCommandParserNameResult.js';
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
import { SlashCommandScope } from './SlashCommandScope.js';
|
import { SlashCommandScope } from './SlashCommandScope.js';
|
||||||
|
|
||||||
@ -17,9 +18,12 @@ export class SlashCommandParser {
|
|||||||
/**@type {Number}*/ index;
|
/**@type {Number}*/ index;
|
||||||
/**@type {SlashCommandScope}*/ scope;
|
/**@type {SlashCommandScope}*/ scope;
|
||||||
|
|
||||||
|
/**@type {{start:number, end:number}[]}*/ closureIndex;
|
||||||
/**@type {SlashCommandExecutor[]}*/ commandIndex;
|
/**@type {SlashCommandExecutor[]}*/ commandIndex;
|
||||||
/**@type {SlashCommandScope[]}*/ scopeIndex;
|
/**@type {SlashCommandScope[]}*/ scopeIndex;
|
||||||
|
|
||||||
|
get userIndex() { return this.index - 2; }
|
||||||
|
|
||||||
get ahead() {
|
get ahead() {
|
||||||
return this.text.slice(this.index + 1);
|
return this.text.slice(this.index + 1);
|
||||||
}
|
}
|
||||||
@ -170,11 +174,10 @@ export class SlashCommandParser {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {*} text
|
* @param {*} text The text to parse.
|
||||||
* @param {*} index
|
* @param {*} index Index to check for names (cursor position).
|
||||||
* @returns {SlashCommandExecutor|String[]}
|
|
||||||
*/
|
*/
|
||||||
getCommandAt(text, index) {
|
getNameAt(text, index) {
|
||||||
try {
|
try {
|
||||||
this.parse(text, false);
|
this.parse(text, false);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -187,9 +190,32 @@ export class SlashCommandParser {
|
|||||||
.slice(-1)[0]
|
.slice(-1)[0]
|
||||||
?? null
|
?? null
|
||||||
;
|
;
|
||||||
const scope = this.scopeIndex[this.commandIndex.indexOf(executor)];
|
|
||||||
if (executor && executor.name == ':') return [executor, scope?.allVariableNames];
|
if (executor) {
|
||||||
return executor;
|
const childClosure = this.closureIndex
|
||||||
|
.find(it=>it.start <= index && (it.end >= index || it.end == null) && it.start > executor.start)
|
||||||
|
?? null
|
||||||
|
;
|
||||||
|
if (childClosure !== null) return null;
|
||||||
|
if (executor.name == ':') {
|
||||||
|
return 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))
|
||||||
|
?? []
|
||||||
|
,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return new SlashCommandParserNameResult(
|
||||||
|
NAME_RESULT_TYPE.COMMAND,
|
||||||
|
executor.name,
|
||||||
|
executor.start,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -260,6 +286,7 @@ export class SlashCommandParser {
|
|||||||
this.keptText = '';
|
this.keptText = '';
|
||||||
this.index = 0;
|
this.index = 0;
|
||||||
this.scope = null;
|
this.scope = null;
|
||||||
|
this.closureIndex = [];
|
||||||
this.commandIndex = [];
|
this.commandIndex = [];
|
||||||
this.scopeIndex = [];
|
this.scopeIndex = [];
|
||||||
const closure = this.parseClosure();
|
const closure = this.parseClosure();
|
||||||
@ -271,10 +298,12 @@ export class SlashCommandParser {
|
|||||||
return this.testSymbol('{:');
|
return this.testSymbol('{:');
|
||||||
}
|
}
|
||||||
testClosureEnd() {
|
testClosureEnd() {
|
||||||
if (this.ahead.length < 1) throw new SlashCommandParserError(`Unclosed closure at position ${this.index - 2}`, this.text, this.index);
|
if (this.ahead.length < 1) throw new SlashCommandParserError(`Unclosed closure at position ${this.userIndex}`, this.text, this.index);
|
||||||
return this.testSymbol(':}');
|
return this.testSymbol(':}');
|
||||||
}
|
}
|
||||||
parseClosure() {
|
parseClosure() {
|
||||||
|
const closureIndexEntry = { start:this.index + 1, end:null };
|
||||||
|
this.closureIndex.push(closureIndexEntry);
|
||||||
let injectPipe = true;
|
let injectPipe = true;
|
||||||
this.take(2); // discard opening {:
|
this.take(2); // discard opening {:
|
||||||
let closure = new SlashCommandClosure(this.scope);
|
let closure = new SlashCommandClosure(this.scope);
|
||||||
@ -316,6 +345,7 @@ export class SlashCommandParser {
|
|||||||
this.take(2); // discard ()
|
this.take(2); // discard ()
|
||||||
closure.executeNow = true;
|
closure.executeNow = true;
|
||||||
}
|
}
|
||||||
|
closureIndexEntry.end = this.index - 1;
|
||||||
this.discardWhitespace(); // discard trailing whitespace
|
this.discardWhitespace(); // discard trailing whitespace
|
||||||
this.scope = closure.scope.parent;
|
this.scope = closure.scope.parent;
|
||||||
return closure;
|
return closure;
|
||||||
@ -352,12 +382,12 @@ export class SlashCommandParser {
|
|||||||
return cmd;
|
return cmd;
|
||||||
} else {
|
} else {
|
||||||
console.warn(this.behind, this.char, this.ahead);
|
console.warn(this.behind, this.char, this.ahead);
|
||||||
throw new SlashCommandParserError(`Unexpected end of command at position ${this.index - 2}: "/${cmd.name}"`, this.text, this.index);
|
throw new SlashCommandParserError(`Unexpected end of command at position ${this.userIndex}: "/${cmd.name}"`, this.text, this.index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
testCommand() {
|
testCommand() {
|
||||||
return this.testSymbol('/') && !this.testSymbol('//') && !this.testSymbol('/#') && !this.testSymbol(':}', 1);
|
return this.testSymbol('/') && !this.testSymbol('//') && !this.testSymbol('/#');
|
||||||
}
|
}
|
||||||
testCommandEnd() {
|
testCommandEnd() {
|
||||||
return this.testClosureEnd() || this.testSymbol('|');
|
return this.testClosureEnd() || this.testSymbol('|');
|
||||||
@ -396,7 +426,7 @@ export class SlashCommandParser {
|
|||||||
return cmd;
|
return cmd;
|
||||||
} else {
|
} else {
|
||||||
console.warn(this.behind, this.char, this.ahead);
|
console.warn(this.behind, this.char, this.ahead);
|
||||||
throw new SlashCommandParserError(`Unexpected end of command at position ${this.index - 2}: "/${cmd.name}"`, this.text, this.index);
|
throw new SlashCommandParserError(`Unexpected end of command at position ${this.userIndex}: "/${cmd.name}"`, this.text, this.index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,32 @@
|
|||||||
|
import { SlashCommandAutoCompleteOption } from './SlashCommandAutoCompleteOption.js';
|
||||||
|
|
||||||
|
|
||||||
|
/**@readonly*/
|
||||||
|
/**@enum {number}*/
|
||||||
|
export const NAME_RESULT_TYPE = {
|
||||||
|
'COMMAND': 1,
|
||||||
|
'CLOSURE': 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export class SlashCommandParserNameResult {
|
||||||
|
/**@type {NAME_RESULT_TYPE} */ type;
|
||||||
|
/**@type {string} */ name;
|
||||||
|
/**@type {number} */ start;
|
||||||
|
/**@type {SlashCommandAutoCompleteOption[]} */ optionList = [];
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {NAME_RESULT_TYPE} type Type of the name at the requested index.
|
||||||
|
* @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.
|
||||||
|
*/
|
||||||
|
constructor(type, name, start, optionList = []) {
|
||||||
|
this.type = type;
|
||||||
|
this.name = name,
|
||||||
|
this.start = start;
|
||||||
|
this.optionList = optionList;
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user