diff --git a/play.pokemonshowdown.com/src/panel-chat.tsx b/play.pokemonshowdown.com/src/panel-chat.tsx index da82cac50..aaba68340 100644 --- a/play.pokemonshowdown.com/src/panel-chat.tsx +++ b/play.pokemonshowdown.com/src/panel-chat.tsx @@ -824,6 +824,19 @@ export class CopyableURLBox extends preact.Component<{ url: string }> { } } +interface UserAutoCompleteCandidate { + type: "user"; + userid: string; + prefixIndex: number; +} + +interface CmdAutoCompleteCandidate { + type: "command"; + command: string; +} + +export type AutoCompleteCandidate = UserAutoCompleteCandidate | CmdAutoCompleteCandidate; + export class ChatTextEntry extends preact.Component<{ room: ChatRoom, onMessage: (msg: string, elem: HTMLElement) => void, onKey: (e: KeyboardEvent) => boolean, left?: number, tinyLayout?: boolean, @@ -834,7 +847,7 @@ export class ChatTextEntry extends preact.Component<{ history: string[] = []; historyIndex = 0; tabComplete: { - candidates: { userid: string, prefixIndex: number }[], + candidates: AutoCompleteCandidate[], candidateIndex: number, /** the text left of the cursor before tab completing */ prefix: string, @@ -1099,7 +1112,7 @@ export class ChatTextEntry extends preact.Component<{ return false; } // TODO - add support for commands tabcomplete - handleTabComplete(reverse: boolean) { + handleTabComplete(reverse: boolean): boolean { // Don't tab complete at the start of the text box. let { value, start, end } = this.getSelection(); if (start !== end || end === 0) return false; @@ -1134,23 +1147,25 @@ export class ChatTextEntry extends preact.Component<{ const match2 = /^([\s\S!/]*?)([A-Za-z0-9][^, \n]* [^, ]*)$/.exec(prefix); if (!match1 && !match2) return true; + const candidates: AutoCompleteCandidate[] = []; const idprefix = (match1 ? toID(match1[2]) : ''); let spaceprefix = (match2 ? match2[2].replace(/[^A-Za-z0-9 ]+/g, '').toLowerCase() : ''); - const candidates: { userid: string, prefixIndex: number }[] = []; if (match2 && (match2[0] === '/' || match2[0] === '!')) spaceprefix = ''; for (const userid in users) { if (spaceprefix && users[userid].slice(1).replace(/[^A-Za-z0-9 ]+/g, '') .toLowerCase() .startsWith(spaceprefix)) { - if (match2) candidates.push({ userid, prefixIndex: match2[1].length }); + if (match2) candidates.push({ type: "user", userid, prefixIndex: match2[1].length }); } else if (idprefix && userid.startsWith(idprefix)) { - if (match1) candidates.push({ userid, prefixIndex: match1[1].length }); + if (match1) candidates.push({ type: "user", userid, prefixIndex: match1[1].length }); } } // Sort by most recent to speak in the chat, or, in the case of a tie, // in alphabetical order. const userActivity = this.props.room.userActivity; candidates.sort((a, b) => { + // command autocomplete options aren't added until after the user autocomplete options are sorted. + if (a.type !== "user" || b.type !== "user") return 0; if (a.prefixIndex !== b.prefixIndex) { // shorter prefix length comes first return a.prefixIndex - b.prefixIndex; @@ -1163,6 +1178,28 @@ export class ChatTextEntry extends preact.Component<{ return (a.userid < b.userid) ? -1 : 1; // alphabetical order }); + const currentLine = prefix.substring(prefix.lastIndexOf('\n') + 1); + const isCommandSearch = (currentLine.startsWith('/') && !currentLine.startsWith('//')) || currentLine.startsWith('!'); + if (isCommandSearch) { + PS.mainmenu.makeQuery('cmdsearch', currentLine, true).then((data: string[]) => { + const cmds = data.sort((a, b) => a.length < b.length ? 1 : -1); + const nextCmd = cmds[cmds.length - 1]; + const newValue = nextCmd + value.substring(end); + this.setValue(newValue, nextCmd.length, nextCmd.length); + const currentCandidates = this.tabComplete?.candidates ?? []; + for (const cmd of cmds) { + currentCandidates.unshift({ type: "command", command: cmd }); + } + this.tabComplete = { + candidates: currentCandidates, + candidateIndex: 0, + prefix: nextCmd, + cursor: nextCmd, + }; + }); + return true; + } + if (!candidates.length) { this.tabComplete = null; return true; @@ -1176,13 +1213,22 @@ export class ChatTextEntry extends preact.Component<{ } // Substitute in the tab-completed name const candidate = this.tabComplete.candidates[this.tabComplete.candidateIndex]; - let name = users[candidate.userid]; - if (!name) return true; + if (candidate.type === "user") { + let name = users[candidate.userid]; + if (!name) return true; - name = Dex.getShortName(name.slice(1)); // Remove rank and busy characters - const cursor = this.tabComplete.prefix.slice(0, candidate.prefixIndex) + name; - this.setValue(cursor + value.slice(end), cursor.length); - this.tabComplete.cursor = cursor; + name = Dex.getShortName(name.slice(1)); // Remove rank and busy characters + const cursor = this.tabComplete.prefix.slice(0, candidate.prefixIndex) + name; + this.setValue(cursor + value.slice(end), cursor.length); + this.tabComplete.cursor = cursor; + } else { + const prefixIndex = prefix.lastIndexOf('\n') + 1; + const fullPrefix = prefix.substring(0, prefixIndex) + Dex.getShortName(candidate.command); + const newValue = fullPrefix + value.substring(end); + this.setValue(newValue, fullPrefix.length, fullPrefix.length); + this.tabComplete.cursor = fullPrefix; + this.tabComplete.prefix = fullPrefix; + } return true; } undoTabComplete() { diff --git a/play.pokemonshowdown.com/src/panel-mainmenu.tsx b/play.pokemonshowdown.com/src/panel-mainmenu.tsx index 52a87e816..53f242f91 100644 --- a/play.pokemonshowdown.com/src/panel-mainmenu.tsx +++ b/play.pokemonshowdown.com/src/panel-mainmenu.tsx @@ -381,9 +381,9 @@ export class MainMenuRoom extends PSRoom { * Most queries are still handled hardcoded, so this is only for certain * special queries that need a Promise. */ - makeQuery(id: string, param?: string) { + makeQuery(id: string, param?: string, excludeParamFromListener?: boolean) { let fullid = id; - if (param) fullid += ` ${toID(param)}`; + if (param && !excludeParamFromListener) fullid += ` ${toID(param)}`; return new Promise(resolve => { if (!this.listeners[fullid]) { this.listeners[fullid] = [];