add minimum requirement of 2 [A-za-z] for slashcommand autocomplete to show up (#4080)

* add minimum requirement of 2 [A-za-z] for slashcommand autocomplete to show up

* Migrate to dedicated AC toggle

* Replace state checkbox with select

---------

Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>
This commit is contained in:
RossAscends
2025-06-02 02:07:33 +09:00
committed by GitHub
parent 4c3bb1aede
commit fa10833e52
4 changed files with 57 additions and 33 deletions

View File

@@ -21,6 +21,14 @@ export const AUTOCOMPLETE_SELECT_KEY = {
'ENTER': 2, // 2^1
};
/** @readonly */
/** @enum {Number} */
export const AUTOCOMPLETE_STATE = {
DISABLED: 0,
MIN_LENGTH: 1,
ALWAYS: 2,
};
export class AutoComplete {
/**@type {HTMLTextAreaElement|HTMLInputElement}*/ textarea;
/**@type {boolean}*/ isFloating = false;
@@ -109,20 +117,20 @@ export class AutoComplete {
this.updateDetailsPositionDebounced = debounce(this.updateDetailsPosition.bind(this), 10);
this.updateFloatingPositionDebounced = debounce(this.updateFloatingPosition.bind(this), 10);
textarea.addEventListener('input', ()=>{
textarea.addEventListener('input', () => {
this.selectionStart = this.textarea.selectionStart;
if (this.text != this.textarea.value) this.show(true, this.wasForced);
});
textarea.addEventListener('keydown', (evt)=>this.handleKeyDown(evt));
textarea.addEventListener('click', ()=>{
textarea.addEventListener('keydown', (evt) => this.handleKeyDown(evt));
textarea.addEventListener('click', () => {
this.selectionStart = this.textarea.selectionStart;
if (this.isActive) this.show();
});
textarea.addEventListener('blur', ()=>this.hide());
textarea.addEventListener('blur', () => this.hide());
if (isFloating) {
textarea.addEventListener('scroll', ()=>this.updateFloatingPositionDebounced());
textarea.addEventListener('scroll', () => this.updateFloatingPositionDebounced());
}
window.addEventListener('resize', ()=>this.updatePositionDebounced());
window.addEventListener('resize', () => this.updatePositionDebounced());
}
/**
@@ -132,9 +140,9 @@ export class AutoComplete {
makeItem(option) {
const li = option.renderItem();
// gotta listen to pointerdown (happens before textarea-blur)
li.addEventListener('pointerdown', (evt)=>{
li.addEventListener('pointerdown', (evt) => {
evt.preventDefault();
this.selectedItem = this.result.find(it=>it.name == li.getAttribute('data-name'));
this.selectedItem = this.result.find(it => it.name == li.getAttribute('data-name'));
this.select();
});
return li;
@@ -149,7 +157,7 @@ export class AutoComplete {
const chars = Array.from(item.dom.querySelector('.name').children);
switch (this.matchType) {
case 'strict': {
chars.forEach((it, idx)=>{
chars.forEach((it, idx) => {
if (idx + item.nameOffset < item.name.length) {
it.classList.add('matched');
} else {
@@ -160,7 +168,7 @@ export class AutoComplete {
}
case 'includes': {
const start = item.name.toLowerCase().search(this.name);
chars.forEach((it, idx)=>{
chars.forEach((it, idx) => {
if (idx + item.nameOffset < start) {
it.classList.remove('matched');
} else if (idx + item.nameOffset < start + item.name.length) {
@@ -172,18 +180,18 @@ export class AutoComplete {
break;
}
case 'fuzzy': {
item.name.replace(this.fuzzyRegex, (_, ...parts)=>{
item.name.replace(this.fuzzyRegex, (_, ...parts) => {
parts.splice(-2, 2);
if (parts.length == 2) {
chars.forEach(c=>c.classList.remove('matched'));
chars.forEach(c => c.classList.remove('matched'));
} else {
let cIdx = item.nameOffset;
parts.forEach((it, idx)=>{
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'));
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'));
chars.slice(cIdx, cIdx + it.length).forEach(c => c.classList.remove('matched'));
}
cIdx += it.length;
});
@@ -230,7 +238,7 @@ export class AutoComplete {
if (current.length > 0) {
consecutive.push(current);
}
consecutive.sort((a,b)=>b.length - a.length);
consecutive.sort((a, b) => b.length - a.length);
option.score = new AutoCompleteFuzzyScore(start, consecutive[0]?.length ?? 0);
return option;
}
@@ -254,8 +262,7 @@ export class AutoComplete {
+ this.parserResult.name.length
+ (this.startQuote ? 1 : 0)
+ (this.endQuote ? 1 : 0)
+ 1
;
+ 1;
}
/**
@@ -344,7 +351,7 @@ export class AutoComplete {
if (this.matchType == 'fuzzy') {
// only build the fuzzy regex if match type is set to fuzzy
this.fuzzyRegex = new RegExp(`^(.*?)${this.name.split('').map(char=>`(${escapeRegex(char)})`).join('(.*?)')}(.*?)$`, 'i');
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
@@ -358,12 +365,12 @@ export class AutoComplete {
// filter the list of options by the partial name according to the matching type
.filter(it => this.isReplaceable || it.name == '' ? (it.matchProvider ? it.matchProvider(this.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);
.filter((it, idx, list) => list.findIndex(opt => opt.value == it.value) == idx);
if (this.result.length == 0 && this.effectiveParserResult != this.parserResult && isForced) {
// no matching secondary results and forced trigger -> show current command details
this.secondaryParserResult = null;
this.result = [this.effectiveParserResult.optionList.find(it=>it.name == this.effectiveParserResult.name)];
this.result = [this.effectiveParserResult.optionList.find(it => it.name == this.effectiveParserResult.name)];
this.name = this.effectiveParserResult.name;
this.fuzzyRegex = /(.*)(.*)(.*)/;
}
@@ -387,8 +394,7 @@ export class AutoComplete {
return option;
})
// sort by fuzzy score or alphabetical
.toSorted(this.matchType == 'fuzzy' ? this.fuzzyScoreCompare : (a, b) => a.name.localeCompare(b.name))
;
.toSorted(this.matchType == 'fuzzy' ? this.fuzzyScoreCompare : (a, b) => a.name.localeCompare(b.name));
@@ -628,12 +634,12 @@ export class AutoComplete {
this.clone.style.position = 'fixed';
this.clone.style.visibility = 'hidden';
document.body.append(this.clone);
const mo = new MutationObserver(muts=>{
if (muts.find(it=>Array.from(it.removedNodes).includes(this.textarea))) {
const mo = new MutationObserver(muts => {
if (muts.find(it => Array.from(it.removedNodes).includes(this.textarea))) {
this.clone.remove();
}
});
mo.observe(this.textarea.parentElement, { childList:true });
mo.observe(this.textarea.parentElement, { childList: true });
}
this.clone.style.height = `${inputRect.height}px`;
this.clone.style.left = `${inputRect.left}px`;
@@ -685,7 +691,7 @@ export class AutoComplete {
this.textarea.selectionDirection = selectionEnd;
}
this.wasForced = false;
this.textarea.dispatchEvent(new Event('input', { bubbles:true }));
this.textarea.dispatchEvent(new Event('input', { bubbles: true }));
this.onSelect?.(this.selectedItem);
}
@@ -700,7 +706,7 @@ export class AutoComplete {
this.selectedItem.dom.classList.add('selected');
const rect = this.selectedItem.dom.children[0].getBoundingClientRect();
const rectParent = this.dom.getBoundingClientRect();
if (rect.top < rectParent.top || rect.bottom > rectParent.bottom ) {
if (rect.top < rectParent.top || rect.bottom > rectParent.bottom) {
this.dom.scrollTop += rect.top < rectParent.top ? rect.top - rectParent.top : rect.bottom - rectParent.bottom;
}
this.renderDetailsDebounced();
@@ -809,8 +815,8 @@ export class AutoComplete {
}
// await keyup to see if cursor position or text has changed
const oldText = this.textarea.value;
await new Promise(resolve=>{
window.addEventListener('keyup', resolve, { once:true });
await new Promise(resolve => {
window.addEventListener('keyup', resolve, { once: true });
});
if (this.selectionStart != this.textarea.selectionStart) {
this.selectionStart = this.textarea.selectionStart;