improve performance

This commit is contained in:
LenAnderson 2024-04-16 16:44:48 -04:00
parent b36740d3a2
commit 001b22bec0
2 changed files with 241 additions and 103 deletions

View File

@ -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 {
return parts.map((it, idx)=>{ let cIdx = 0;
parts.forEach((it, idx)=>{
if (it === null || it.length == 0) return ''; if (it === null || it.length == 0) return '';
if (idx % 2 == 1) { if (idx % 2 == 1) {
return `<span class="matched">${it}</span>`; 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'));
} }
return it; cIdx += it.length;
}).join(''); });
}
return '';
}); });
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; frag.append(item.dom);
// 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);
} }
} dom.append(frag);
selectedItem = result[0];
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);
if (!clone) {
clone = document.createElement('div');
for (const key of style) { for (const key of style) {
clone.style[key] = style[key]; 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);
}); });
textarea.addEventListener('blur', ()=>hide()); if (selectionStart != textarea.selectionStart) {
selectionStart = textarea.selectionStart;
showAutoCompleteDebounced(oldText != textarea.value);
}
}
}
});
// textarea.addEventListener('blur', ()=>hide());
if (isFloating) { if (isFloating) {
textarea.addEventListener('scroll', debounce(updateFloatingPosition, 100)); textarea.addEventListener('scroll', debounce(updateFloatingPosition, 100));
} }

View File

@ -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;
/** /**