improve performance
This commit is contained in:
parent
b36740d3a2
commit
001b22bec0
|
@ -57,7 +57,7 @@ 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 { NAME_RESULT_TYPE, SlashCommandParserNameResult } from './slash-commands/SlashCommandParserNameResult.js';
|
||||||
import { OPTION_TYPE, SlashCommandAutoCompleteOption } from './slash-commands/SlashCommandAutoCompleteOption.js';
|
import { OPTION_TYPE, SlashCommandAutoCompleteOption, SlashCommandFuzzyScore } from './slash-commands/SlashCommandAutoCompleteOption.js';
|
||||||
export {
|
export {
|
||||||
executeSlashCommands, getSlashCommandsHelp, registerSlashCommand,
|
executeSlashCommands, getSlashCommandsHelp, registerSlashCommand,
|
||||||
};
|
};
|
||||||
|
@ -1795,6 +1795,7 @@ export async function setSlashCommandAutoComplete(textarea, isFloating = false)
|
||||||
dom.classList.add('slashCommandAutoComplete');
|
dom.classList.add('slashCommandAutoComplete');
|
||||||
}
|
}
|
||||||
let isReplacable = false;
|
let isReplacable = false;
|
||||||
|
/**@type {SlashCommandAutoCompleteOption[]} */
|
||||||
let result = [];
|
let result = [];
|
||||||
let selectedItem = null;
|
let selectedItem = null;
|
||||||
let isActive = false;
|
let isActive = false;
|
||||||
|
@ -1804,6 +1805,67 @@ export async function setSlashCommandAutoComplete(textarea, isFloating = false)
|
||||||
let clone;
|
let clone;
|
||||||
let startQuote;
|
let startQuote;
|
||||||
let endQuote;
|
let endQuote;
|
||||||
|
/**@type {Object.<string,HTMLElement>} */
|
||||||
|
const items = {};
|
||||||
|
let hasCache = false;
|
||||||
|
let selectionStart;
|
||||||
|
const makeItem = (key, typeIcon, noSlash, helpString = '', aliasList = []) => {
|
||||||
|
const li = document.createElement('li'); {
|
||||||
|
li.classList.add('item');
|
||||||
|
const type = document.createElement('span'); {
|
||||||
|
type.classList.add('type');
|
||||||
|
type.classList.add('monospace');
|
||||||
|
type.textContent = typeIcon;
|
||||||
|
li.append(type);
|
||||||
|
}
|
||||||
|
const name = document.createElement('span'); {
|
||||||
|
name.classList.add('name');
|
||||||
|
name.classList.add('monospace');
|
||||||
|
name.textContent = noSlash ? '' : '/';
|
||||||
|
key.split('').forEach(char=>{
|
||||||
|
const span = document.createElement('span'); {
|
||||||
|
span.textContent = char;
|
||||||
|
name.append(span);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
li.append(name);
|
||||||
|
}
|
||||||
|
li.append(' ');
|
||||||
|
const help = document.createElement('span'); {
|
||||||
|
help.classList.add('help');
|
||||||
|
help.innerHTML = helpString;
|
||||||
|
li.append(help);
|
||||||
|
}
|
||||||
|
if (aliasList.length > 0) {
|
||||||
|
const aliases = document.createElement('span'); {
|
||||||
|
aliases.classList.add('aliases');
|
||||||
|
aliases.append(' (alias: ');
|
||||||
|
for (const aliasName of aliasList) {
|
||||||
|
const alias = document.createElement('span'); {
|
||||||
|
alias.classList.add('monospace');
|
||||||
|
alias.textContent = `/${aliasName}`;
|
||||||
|
aliases.append(alias);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
aliases.append(')');
|
||||||
|
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)
|
||||||
|
pointerup = new Promise(resolve=>{
|
||||||
|
const resolver = ()=>{
|
||||||
|
window.removeEventListener('pointerup', resolver);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
window.addEventListener('pointerup', resolver);
|
||||||
|
});
|
||||||
|
select();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return li;
|
||||||
|
};
|
||||||
const hide = () => {
|
const hide = () => {
|
||||||
dom?.remove();
|
dom?.remove();
|
||||||
isActive = false;
|
isActive = false;
|
||||||
|
@ -1816,6 +1878,15 @@ export async function setSlashCommandAutoComplete(textarea, isFloating = false)
|
||||||
// only show for slash commands
|
// only show for slash commands
|
||||||
if (text[0] != '/') return hide();
|
if (text[0] != '/') return hide();
|
||||||
|
|
||||||
|
if (!hasCache) {
|
||||||
|
hasCache = true;
|
||||||
|
// init by appending all command options
|
||||||
|
Object.keys(parser.commands).forEach(key=>{
|
||||||
|
const cmd = parser.commands[key];
|
||||||
|
items[key] = makeItem(key, '/', false, cmd.helpString, [cmd.name, ...(cmd.aliases ?? [])].filter(it=>it != key));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 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
|
||||||
parserResult = parser.getNameAt(text, textarea.selectionStart);
|
parserResult = parser.getNameAt(text, textarea.selectionStart);
|
||||||
|
@ -1833,13 +1904,16 @@ export async function setSlashCommandAutoComplete(textarea, isFloating = false)
|
||||||
} catch { /* empty */ }
|
} catch { /* empty */ }
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default: // no result -> empty slash "/" -> list all commands
|
|
||||||
case NAME_RESULT_TYPE.COMMAND: {
|
case NAME_RESULT_TYPE.COMMAND: {
|
||||||
parserResult.optionList.push(...Object.keys(parser.commands)
|
parserResult.optionList.push(...Object.keys(parser.commands)
|
||||||
.map(key=>new SlashCommandAutoCompleteOption(OPTION_TYPE.COMMAND, parser.commands[key], key)),
|
.map(key=>new SlashCommandAutoCompleteOption(OPTION_TYPE.COMMAND, parser.commands[key], key)),
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
default: {
|
||||||
|
// no result
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let slashCommand = parserResult?.name?.toLowerCase() ?? '';
|
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
|
||||||
|
@ -1849,14 +1923,14 @@ export async function setSlashCommandAutoComplete(textarea, isFloating = false)
|
||||||
isReplacable = isInput && (!parserResult ? true : textarea.selectionStart == parserResult.start - 2 + parserResult.name.length + (startQuote ? 1 : 0));
|
isReplacable = isInput && (!parserResult ? true : textarea.selectionStart == parserResult.start - 2 + parserResult.name.length + (startQuote ? 1 : 0));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default: // no result -> empty slash "/" -> list all commands
|
default: // no result
|
||||||
case NAME_RESULT_TYPE.COMMAND: {
|
case NAME_RESULT_TYPE.COMMAND: {
|
||||||
isReplacable = isInput && (!parserResult ? true : textarea.selectionStart == parserResult.start - 2 + parserResult.name.length);
|
isReplacable = isInput && (!parserResult ? true : textarea.selectionStart == parserResult.start - 2 + parserResult.name.length);
|
||||||
break;
|
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) {
|
switch (parserResult?.type) {
|
||||||
case NAME_RESULT_TYPE.CLOSURE: {
|
case NAME_RESULT_TYPE.CLOSURE: {
|
||||||
|
@ -1867,7 +1941,6 @@ export async function setSlashCommandAutoComplete(textarea, isFloating = false)
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default: // no result -> empty slash "/" -> list all commands
|
|
||||||
case NAME_RESULT_TYPE.COMMAND: {
|
case NAME_RESULT_TYPE.COMMAND: {
|
||||||
if (textarea.selectionStart >= parserResult.start - 2 && textarea.selectionStart <= parserResult.start - 2 + parserResult.name.length) {
|
if (textarea.selectionStart >= parserResult.start - 2 && textarea.selectionStart <= parserResult.start - 2 + parserResult.name.length) {
|
||||||
slashCommand = slashCommand.slice(0, textarea.selectionStart - (parserResult.start - 2));
|
slashCommand = slashCommand.slice(0, textarea.selectionStart - (parserResult.start - 2));
|
||||||
|
@ -1876,6 +1949,10 @@ export async function setSlashCommandAutoComplete(textarea, isFloating = false)
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
default: {
|
||||||
|
// no result
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1886,8 +1963,13 @@ export async function setSlashCommandAutoComplete(textarea, isFloating = false)
|
||||||
'includes': (name) => name.toLowerCase().includes(slashCommand),
|
'includes': (name) => name.toLowerCase().includes(slashCommand),
|
||||||
'fuzzy': (name) => fuzzyRegex.test(name),
|
'fuzzy': (name) => fuzzyRegex.test(name),
|
||||||
};
|
};
|
||||||
const fuzzyScore = (name) => {
|
/**
|
||||||
const parts = fuzzyRegex.exec(name).slice(1, -1);
|
*
|
||||||
|
* @param {SlashCommandAutoCompleteOption} option
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const fuzzyScore = (option) => {
|
||||||
|
const parts = fuzzyRegex.exec(option.name).slice(1, -1);
|
||||||
let start = null;
|
let start = null;
|
||||||
let consecutive = [];
|
let consecutive = [];
|
||||||
let current = '';
|
let current = '';
|
||||||
|
@ -1912,8 +1994,13 @@ export async function setSlashCommandAutoComplete(textarea, isFloating = false)
|
||||||
consecutive.push(current);
|
consecutive.push(current);
|
||||||
}
|
}
|
||||||
consecutive.sort((a,b)=>b.length - a.length);
|
consecutive.sort((a,b)=>b.length - a.length);
|
||||||
return { name, start, longestConsecutive:consecutive[0]?.length ?? 0 };
|
option.score = new SlashCommandFuzzyScore(start, consecutive[0]?.length ?? 0);
|
||||||
|
return option;
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* @param {SlashCommandAutoCompleteOption} a
|
||||||
|
* @param {SlashCommandAutoCompleteOption} b
|
||||||
|
*/
|
||||||
const fuzzyScoreCompare = (a, b) => {
|
const fuzzyScoreCompare = (a, b) => {
|
||||||
if (a.score.start < b.score.start) return -1;
|
if (a.score.start < b.score.start) return -1;
|
||||||
if (a.score.start > b.score.start) return 1;
|
if (a.score.start > b.score.start) return 1;
|
||||||
|
@ -1921,81 +2008,90 @@ export async function setSlashCommandAutoComplete(textarea, isFloating = false)
|
||||||
if (a.score.longestConsecutive < b.score.longestConsecutive) return 1;
|
if (a.score.longestConsecutive < b.score.longestConsecutive) return 1;
|
||||||
return a.name.localeCompare(b.name);
|
return a.name.localeCompare(b.name);
|
||||||
};
|
};
|
||||||
const buildHelpStringName = (name, noSlash=false) => {
|
/**
|
||||||
|
*
|
||||||
|
* @param {SlashCommandAutoCompleteOption} item
|
||||||
|
*/
|
||||||
|
const updateName = (item) => {
|
||||||
|
const chars = Array.from(item.dom.querySelector('.name').children);
|
||||||
switch (matchType) {
|
switch (matchType) {
|
||||||
case 'strict': {
|
case 'strict': {
|
||||||
return `<span class="monospace">${noSlash?'':'/'}<span class="matched">${name.slice(0, slashCommand.length)}</span>${name.slice(slashCommand.length)}</span> `;
|
chars.forEach((it, idx)=>{
|
||||||
|
if (idx < item.name.length) {
|
||||||
|
it.classList.add('matched');
|
||||||
|
} else {
|
||||||
|
it.classList.remove('matched');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
case 'includes': {
|
case 'includes': {
|
||||||
const start = name.toLowerCase().search(slashCommand);
|
const start = item.name.toLowerCase().search(slashCommand);
|
||||||
return `<span class="monospace">${noSlash?'':'/'}${name.slice(0, start)}<span class="matched">${name.slice(start, start + slashCommand.length)}</span>${name.slice(start + slashCommand.length)}</span> `;
|
chars.forEach((it, idx)=>{
|
||||||
|
if (idx < start) {
|
||||||
|
it.classList.remove('matched');
|
||||||
|
} else if (idx < start + item.name.length) {
|
||||||
|
it.classList.add('matched');
|
||||||
|
} else {
|
||||||
|
it.classList.remove('matched');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
case 'fuzzy': {
|
case 'fuzzy': {
|
||||||
const matched = name.replace(fuzzyRegex, (_, ...parts)=>{
|
item.name.replace(fuzzyRegex, (_, ...parts)=>{
|
||||||
parts.splice(-2, 2);
|
parts.splice(-2, 2);
|
||||||
if (parts.length == 2) {
|
if (parts.length == 2) {
|
||||||
return parts.join('');
|
chars.forEach(c=>c.classList.remove('matched'));
|
||||||
|
} else {
|
||||||
|
let cIdx = 0;
|
||||||
|
parts.forEach((it, idx)=>{
|
||||||
|
if (it === null || it.length == 0) return '';
|
||||||
|
if (idx % 2 == 1) {
|
||||||
|
chars.slice(cIdx, cIdx + it.length).forEach(c=>c.classList.add('matched'));
|
||||||
|
} else {
|
||||||
|
chars.slice(cIdx, cIdx + it.length).forEach(c=>c.classList.remove('matched'));
|
||||||
|
}
|
||||||
|
cIdx += it.length;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return parts.map((it, idx)=>{
|
return '';
|
||||||
if (it === null || it.length == 0) return '';
|
|
||||||
if (idx % 2 == 1) {
|
|
||||||
return `<span class="matched">${it}</span>`;
|
|
||||||
}
|
|
||||||
return it;
|
|
||||||
}).join('');
|
|
||||||
});
|
});
|
||||||
return `<span class="monospace">${noSlash?'':'/'}${matched}</span> `;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return item;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 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 (!parserResult) return hide();
|
if (!parserResult) return hide();
|
||||||
else {
|
else {
|
||||||
const matchingOptions = parserResult.optionList
|
let matchingOptions = parserResult.optionList
|
||||||
.filter(it => isReplacable || it.name == '' ? matchers[matchType](it.name) : it.name.toLowerCase() == slashCommand) // Filter by the input
|
.filter(it => isReplacable || it.name == '' ? matchers[matchType](it.name) : it.name.toLowerCase() == slashCommand) // Filter by the input
|
||||||
|
.filter((it,idx,list) => list.indexOf(it) == idx)
|
||||||
;
|
;
|
||||||
result = matchingOptions
|
result = matchingOptions
|
||||||
.filter((it,idx) => matchingOptions.indexOf(it) == idx)
|
.filter((it,idx) => matchingOptions.indexOf(it) == idx)
|
||||||
.map(option => {
|
.map(option => {
|
||||||
let typeIcon = '';
|
let li;
|
||||||
let noSlash = false;
|
|
||||||
let helpString = '';
|
|
||||||
let aliases = '';
|
|
||||||
switch (option.type) {
|
switch (option.type) {
|
||||||
case OPTION_TYPE.QUICK_REPLY: {
|
case OPTION_TYPE.QUICK_REPLY: {
|
||||||
typeIcon = 'QR';
|
li = makeItem(option.name, 'QR', true);
|
||||||
noSlash = true;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case OPTION_TYPE.VARIABLE_NAME: {
|
case OPTION_TYPE.VARIABLE_NAME: {
|
||||||
typeIcon = '𝑥';
|
li = makeItem(option.name, '𝑥', true);
|
||||||
noSlash = true;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case OPTION_TYPE.COMMAND: {
|
case OPTION_TYPE.COMMAND: {
|
||||||
typeIcon = '/';
|
li = items[option.name];
|
||||||
noSlash = false;
|
|
||||||
helpString = option.value.helpString;
|
|
||||||
if (option.value.aliases.length > 0) {
|
|
||||||
aliases = ' (alias: ';
|
|
||||||
aliases += [option.value.name, ...option.value.aliases]
|
|
||||||
.filter(it=>it != option)
|
|
||||||
.map(it=>`<span class="monospace">/${it}</span>`)
|
|
||||||
.join(', ')
|
|
||||||
;
|
|
||||||
aliases += ')';
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
option.replacer = option.name.includes(' ') || startQuote || endQuote ? `"${option.name}"` : `${option.name}`;
|
||||||
name: option.name,
|
option.dom = li;
|
||||||
label: `<span class="type monospace">${typeIcon}</span> ${buildHelpStringName(option.name, noSlash)}${helpString}${aliases}`,
|
if (matchType == 'fuzzy') fuzzyScore(option);
|
||||||
value: option.name.includes(' ') || startQuote || endQuote ? `"${option.name}"` : `${option.name}`,
|
updateName(option);
|
||||||
score: matchType == 'fuzzy' ? fuzzyScore(option.name) : null,
|
return option;
|
||||||
li: null,
|
|
||||||
};
|
|
||||||
}) // Map to the help string and score
|
}) // Map to the help string and score
|
||||||
.toSorted(matchType == 'fuzzy' ? fuzzyScoreCompare : (a, b) => a.name.localeCompare(b.name)) // sort by score (if fuzzy) or name
|
.toSorted(matchType == 'fuzzy' ? fuzzyScoreCompare : (a, b) => a.name.localeCompare(b.name)) // sort by score (if fuzzy) or name
|
||||||
;
|
;
|
||||||
|
@ -2007,26 +2103,30 @@ export async function setSlashCommandAutoComplete(textarea, isFloating = false)
|
||||||
return hide();
|
return hide();
|
||||||
}
|
}
|
||||||
// otherwise add "no match" notice
|
// otherwise add "no match" notice
|
||||||
switch (parserResult.type) {
|
switch (parserResult?.type) {
|
||||||
case NAME_RESULT_TYPE.CLOSURE: {
|
case NAME_RESULT_TYPE.CLOSURE: {
|
||||||
|
const li = document.createElement('li'); {
|
||||||
|
li.textContent = slashCommand.length ?
|
||||||
|
`No matching variables in scope and no matching Quick Replies for "${slashCommand}"`
|
||||||
|
: 'No variables in scope and no Quick Replies found.';
|
||||||
|
}
|
||||||
result.push({
|
result.push({
|
||||||
name: '',
|
name: '',
|
||||||
label: slashCommand.length ?
|
|
||||||
`No matching variables in scope and no matching Quick Replies for "${slashCommand}"`
|
|
||||||
: 'No variables in scope and no Quick Replies found.',
|
|
||||||
value: null,
|
value: null,
|
||||||
score: null,
|
score: null,
|
||||||
li: null,
|
li,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case NAME_RESULT_TYPE.COMMAND: {
|
case NAME_RESULT_TYPE.COMMAND: {
|
||||||
|
const li = document.createElement('li'); {
|
||||||
|
li.textContent = `No matching commands for "/${slashCommand}"`;
|
||||||
|
}
|
||||||
result.push({
|
result.push({
|
||||||
name: '',
|
name: '',
|
||||||
label: `No matching commands for "/${slashCommand}"`,
|
|
||||||
value: null,
|
value: null,
|
||||||
score: null,
|
score: null,
|
||||||
li: null,
|
li,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -2037,7 +2137,11 @@ export async function setSlashCommandAutoComplete(textarea, isFloating = false)
|
||||||
} else if (!isReplacable && result.length > 1) {
|
} else if (!isReplacable && result.length > 1) {
|
||||||
return hide();
|
return hide();
|
||||||
}
|
}
|
||||||
|
selectedItem = result[0];
|
||||||
|
isActive = true;
|
||||||
|
renderDebounced();
|
||||||
|
};
|
||||||
|
const render = ()=>{
|
||||||
// render autocomplete list
|
// render autocomplete list
|
||||||
dom.innerHTML = '';
|
dom.innerHTML = '';
|
||||||
dom.classList.remove('defaultDark');
|
dom.classList.remove('defaultDark');
|
||||||
|
@ -2058,35 +2162,21 @@ export async function setSlashCommandAutoComplete(textarea, isFloating = false)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const frag = document.createDocumentFragment();
|
||||||
for (const item of result) {
|
for (const item of result) {
|
||||||
const li = document.createElement('li'); {
|
if (item == selectedItem) {
|
||||||
li.classList.add('item');
|
item.dom.classList.add('selected');
|
||||||
if (item == result[0]) {
|
} else {
|
||||||
li.classList.add('selected');
|
item.dom.classList.remove('selected');
|
||||||
}
|
|
||||||
li.innerHTML = item.label;
|
|
||||||
// gotta listen to pointerdown (happens before textarea-blur)
|
|
||||||
li.addEventListener('pointerdown', ()=>{
|
|
||||||
// gotta catch pointerup to restore focus to textarea (blurs after pointerdown)
|
|
||||||
pointerup = new Promise(resolve=>{
|
|
||||||
const resolver = ()=>{
|
|
||||||
window.removeEventListener('pointerup', resolver);
|
|
||||||
resolve();
|
|
||||||
};
|
|
||||||
window.addEventListener('pointerup', resolver);
|
|
||||||
});
|
|
||||||
selectedItem = item;
|
|
||||||
select();
|
|
||||||
});
|
|
||||||
item.li = li;
|
|
||||||
dom.append(li);
|
|
||||||
}
|
}
|
||||||
|
frag.append(item.dom);
|
||||||
}
|
}
|
||||||
selectedItem = result[0];
|
dom.append(frag);
|
||||||
updatePosition();
|
updatePosition();
|
||||||
document.body.append(dom);
|
document.body.append(dom);
|
||||||
isActive = true;
|
// prevType = parserResult.type;
|
||||||
};
|
};
|
||||||
|
const renderDebounced = debounce(render, 100);
|
||||||
const updatePosition = () => {
|
const updatePosition = () => {
|
||||||
if (isFloating) {
|
if (isFloating) {
|
||||||
updateFloatingPosition();
|
updateFloatingPosition();
|
||||||
|
@ -2127,17 +2217,28 @@ export async function setSlashCommandAutoComplete(textarea, isFloating = false)
|
||||||
*/
|
*/
|
||||||
const getCursorPosition = () => {
|
const getCursorPosition = () => {
|
||||||
const inputRect = textarea.getBoundingClientRect();
|
const inputRect = textarea.getBoundingClientRect();
|
||||||
clone?.remove();
|
// clone?.remove();
|
||||||
clone = document.createElement('div');
|
|
||||||
const style = window.getComputedStyle(textarea);
|
const style = window.getComputedStyle(textarea);
|
||||||
for (const key of style) {
|
if (!clone) {
|
||||||
clone.style[key] = style[key];
|
clone = document.createElement('div');
|
||||||
|
for (const key of style) {
|
||||||
|
clone.style[key] = style[key];
|
||||||
|
}
|
||||||
|
clone.style.position = 'fixed';
|
||||||
|
clone.style.visibility = 'hidden';
|
||||||
|
document.body.append(clone);
|
||||||
|
const mo = new MutationObserver(muts=>{
|
||||||
|
if (muts.find(it=>Array.from(it.removedNodes).includes(textarea))) {
|
||||||
|
clone.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
mo.observe(textarea.parentElement, { childList:true });
|
||||||
}
|
}
|
||||||
clone.style.height = `${inputRect.height}px`;
|
clone.style.height = `${inputRect.height}px`;
|
||||||
clone.style.left = `${inputRect.left}px`;
|
clone.style.left = `${inputRect.left}px`;
|
||||||
clone.style.top = `${inputRect.top}px`;
|
clone.style.top = `${inputRect.top}px`;
|
||||||
clone.style.position = 'fixed';
|
clone.style.whiteSpace = style.whiteSpace;
|
||||||
clone.style.visibility = 'hidden';
|
clone.style.tabSize = style.tabSize;
|
||||||
const text = textarea.value;
|
const text = textarea.value;
|
||||||
const before = text.slice(0, textarea.selectionStart);
|
const before = text.slice(0, textarea.selectionStart);
|
||||||
clone.textContent = before;
|
clone.textContent = before;
|
||||||
|
@ -2145,7 +2246,6 @@ export async function setSlashCommandAutoComplete(textarea, isFloating = false)
|
||||||
locator.textContent = text[textarea.selectionStart];
|
locator.textContent = text[textarea.selectionStart];
|
||||||
clone.append(locator);
|
clone.append(locator);
|
||||||
clone.append(text.slice(textarea.selectionStart + 1));
|
clone.append(text.slice(textarea.selectionStart + 1));
|
||||||
document.body.append(clone);
|
|
||||||
clone.scrollTop = textarea.scrollTop;
|
clone.scrollTop = textarea.scrollTop;
|
||||||
clone.scrollLeft = textarea.scrollLeft;
|
clone.scrollLeft = textarea.scrollLeft;
|
||||||
const locatorRect = locator.getBoundingClientRect();
|
const locatorRect = locator.getBoundingClientRect();
|
||||||
|
@ -2154,24 +2254,25 @@ export async function setSlashCommandAutoComplete(textarea, isFloating = false)
|
||||||
top: locatorRect.top,
|
top: locatorRect.top,
|
||||||
bottom: locatorRect.bottom,
|
bottom: locatorRect.bottom,
|
||||||
};
|
};
|
||||||
clone.remove();
|
// clone.remove();
|
||||||
return location;
|
return location;
|
||||||
};
|
};
|
||||||
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, parserResult.start - 2)}${selectedItem.value}${text.slice(parserResult.start - 2 + parserResult.name.length + (startQuote ? 1 : 0) + (endQuote ? 1 : 0))}`;
|
textarea.value = `${text.slice(0, parserResult.start - 2)}${selectedItem.replacer}${text.slice(parserResult.start - 2 + parserResult.name.length + (startQuote ? 1 : 0) + (endQuote ? 1 : 0))}`;
|
||||||
await pointerup;
|
await pointerup;
|
||||||
textarea.focus();
|
textarea.focus();
|
||||||
textarea.selectionStart = parserResult.start - 2 + selectedItem.value.length;
|
textarea.selectionStart = parserResult.start - 2 + selectedItem.replacer.length;
|
||||||
textarea.selectionEnd = textarea.selectionStart;
|
textarea.selectionEnd = textarea.selectionStart;
|
||||||
show();
|
show();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const showAutoCompleteDebounced = debounce(show, 100);
|
const showAutoCompleteDebounced = show;
|
||||||
textarea.addEventListener('input', ()=>showAutoCompleteDebounced(true));
|
textarea.addEventListener('input', ()=>showAutoCompleteDebounced(true));
|
||||||
textarea.addEventListener('click', ()=>showAutoCompleteDebounced());
|
textarea.addEventListener('click', ()=>showAutoCompleteDebounced());
|
||||||
textarea.addEventListener('keydown', (evt)=>{
|
textarea.addEventListener('selectionchange', ()=>showAutoCompleteDebounced());
|
||||||
|
textarea.addEventListener('keydown', async(evt)=>{
|
||||||
// autocomplete is shown and cursor at end of current command name (or inside name and typed or forced)
|
// autocomplete is shown and cursor at end of current command name (or inside name and typed or forced)
|
||||||
if (isActive && isReplacable) {
|
if (isActive && isReplacable) {
|
||||||
switch (evt.key) {
|
switch (evt.key) {
|
||||||
|
@ -2184,13 +2285,13 @@ export async function setSlashCommandAutoComplete(textarea, isFloating = false)
|
||||||
let newIdx;
|
let newIdx;
|
||||||
if (idx == 0) newIdx = result.length - 1;
|
if (idx == 0) newIdx = result.length - 1;
|
||||||
else newIdx = idx - 1;
|
else newIdx = idx - 1;
|
||||||
selectedItem.li.classList.remove('selected');
|
selectedItem.dom.classList.remove('selected');
|
||||||
selectedItem = result[newIdx];
|
selectedItem = result[newIdx];
|
||||||
selectedItem.li.classList.add('selected');
|
selectedItem.dom.classList.add('selected');
|
||||||
const rect = selectedItem.li.getBoundingClientRect();
|
const rect = selectedItem.dom.getBoundingClientRect();
|
||||||
const rectParent = dom.getBoundingClientRect();
|
const rectParent = dom.getBoundingClientRect();
|
||||||
if (rect.top < rectParent.top || rect.bottom > rectParent.bottom ) {
|
if (rect.top < rectParent.top || rect.bottom > rectParent.bottom ) {
|
||||||
selectedItem.li.scrollIntoView();
|
selectedItem.dom.scrollIntoView();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -2201,13 +2302,13 @@ export async function setSlashCommandAutoComplete(textarea, isFloating = false)
|
||||||
evt.stopPropagation();
|
evt.stopPropagation();
|
||||||
const idx = result.indexOf(selectedItem);
|
const idx = result.indexOf(selectedItem);
|
||||||
const newIdx = (idx + 1) % result.length;
|
const newIdx = (idx + 1) % result.length;
|
||||||
selectedItem.li.classList.remove('selected');
|
selectedItem.dom.classList.remove('selected');
|
||||||
selectedItem = result[newIdx];
|
selectedItem = result[newIdx];
|
||||||
selectedItem.li.classList.add('selected');
|
selectedItem.dom.classList.add('selected');
|
||||||
const rect = selectedItem.li.getBoundingClientRect();
|
const rect = selectedItem.dom.getBoundingClientRect();
|
||||||
const rectParent = dom.getBoundingClientRect();
|
const rectParent = dom.getBoundingClientRect();
|
||||||
if (rect.top < rectParent.top || rect.bottom > rectParent.bottom ) {
|
if (rect.top < rectParent.top || rect.bottom > rectParent.bottom ) {
|
||||||
selectedItem.li.scrollIntoView();
|
selectedItem.dom.scrollIntoView();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -2258,9 +2359,29 @@ export async function setSlashCommandAutoComplete(textarea, isFloating = false)
|
||||||
// ignore keydown on modifier keys
|
// ignore keydown on modifier keys
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
showAutoCompleteDebounced();
|
switch (evt.key) {
|
||||||
|
default:
|
||||||
|
case 'ArrowUp':
|
||||||
|
case 'ArrowDown':
|
||||||
|
case 'ArrowRight':
|
||||||
|
case 'ArrowLeft': {
|
||||||
|
// keyboard navigation, wait for keyup to complete cursor move
|
||||||
|
const oldText = textarea.value;
|
||||||
|
await new Promise(resolve=>{
|
||||||
|
const keyUpListener = ()=>{
|
||||||
|
window.removeEventListener('keyup', keyUpListener);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
window.addEventListener('keyup', keyUpListener);
|
||||||
|
});
|
||||||
|
if (selectionStart != textarea.selectionStart) {
|
||||||
|
selectionStart = textarea.selectionStart;
|
||||||
|
showAutoCompleteDebounced(oldText != textarea.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
textarea.addEventListener('blur', ()=>hide());
|
// textarea.addEventListener('blur', ()=>hide());
|
||||||
if (isFloating) {
|
if (isFloating) {
|
||||||
textarea.addEventListener('scroll', debounce(updateFloatingPosition, 100));
|
textarea.addEventListener('scroll', debounce(updateFloatingPosition, 100));
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,11 +10,28 @@ export const OPTION_TYPE = {
|
||||||
'VARIABLE_NAME': 3,
|
'VARIABLE_NAME': 3,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export class SlashCommandFuzzyScore {
|
||||||
|
/**@type {number}*/ start;
|
||||||
|
/**@type {number}*/ longestConsecutive;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} start
|
||||||
|
* @param {number} longestConsecutive
|
||||||
|
*/
|
||||||
|
constructor(start, longestConsecutive) {
|
||||||
|
this.start = start;
|
||||||
|
this.longestConsecutive = longestConsecutive;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export class SlashCommandAutoCompleteOption {
|
export class SlashCommandAutoCompleteOption {
|
||||||
/**@type {OPTION_TYPE}*/ type;
|
/**@type {OPTION_TYPE}*/ type;
|
||||||
/**@type {string|SlashCommand}*/ value;
|
/**@type {string|SlashCommand}*/ value;
|
||||||
/**@type {string}*/ name;
|
/**@type {string}*/ name;
|
||||||
|
/**@type {SlashCommandFuzzyScore}*/ score;
|
||||||
|
/**@type {string}*/ replacer;
|
||||||
|
/**@type {HTMLElement}*/ dom;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in New Issue