u
This commit is contained in:
117
src/autocomplete-input.js
Normal file
117
src/autocomplete-input.js
Normal file
@@ -0,0 +1,117 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Slash-command autocomplete prompt.
|
||||
*
|
||||
* An `input`-style single-line prompt built on @inquirer/core. It behaves like
|
||||
* a normal text input, but as soon as the buffer starts with "/" (and no
|
||||
* arguments have been typed yet) it renders the matching slash commands as
|
||||
* inline help below the prompt.
|
||||
*
|
||||
* ↑ / ↓ move the highlight between matches
|
||||
* Tab complete the highlighted command into the buffer
|
||||
* Enter submit the buffer (like a normal input)
|
||||
*
|
||||
* The filter term is frozen while the menu is open, so completing a command
|
||||
* with Tab does not collapse the list — repeated ↑/↓ + Tab still works.
|
||||
*/
|
||||
|
||||
import chalk from 'chalk';
|
||||
import {
|
||||
createPrompt,
|
||||
useState,
|
||||
useKeypress,
|
||||
usePrefix,
|
||||
isEnterKey,
|
||||
isTabKey,
|
||||
isUpKey,
|
||||
isDownKey,
|
||||
makeTheme,
|
||||
} from '@inquirer/core';
|
||||
|
||||
import { filterCommands } from './slash-commands.js';
|
||||
|
||||
const MAX_SUGGESTIONS = 6;
|
||||
|
||||
/** Show the inline menu only while the first word is being typed (no space yet). */
|
||||
function termFrom(line) {
|
||||
if (typeof line !== 'string' || !line.startsWith('/')) return null;
|
||||
if (line.includes(' ')) return null;
|
||||
return line.toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace readline's actual edit buffer, not just the visible terminal line.
|
||||
* `rl.clearLine()` only erases terminal output; using it before `rl.write()`
|
||||
* leaves the old buffer in place and turns a completion such as `/h` into
|
||||
* `/h/help` internally.
|
||||
*/
|
||||
function replaceBuffer(rl, nextValue) {
|
||||
rl.write(null, { ctrl: true, name: 'a' });
|
||||
rl.write(null, { ctrl: true, name: 'k' });
|
||||
rl.write(nextValue);
|
||||
}
|
||||
|
||||
export const autocompleteInput = createPrompt((config, done) => {
|
||||
const theme = makeTheme({}, config.theme);
|
||||
const [status, setStatus] = useState('idle');
|
||||
const [value, setValue] = useState('');
|
||||
const [term, setTerm] = useState(null); // frozen filter term while the menu is open
|
||||
const [active, setActive] = useState(0); // highlighted suggestion index
|
||||
|
||||
const prefix = usePrefix({ status, theme });
|
||||
const suggestions = term === null ? [] : filterCommands(term).slice(0, MAX_SUGGESTIONS);
|
||||
const count = suggestions.length;
|
||||
|
||||
useKeypress((key, rl) => {
|
||||
if (status !== 'idle') return;
|
||||
|
||||
if (isEnterKey(key)) {
|
||||
const answer = value || rl.line;
|
||||
setValue(answer);
|
||||
setStatus('done');
|
||||
done(answer);
|
||||
return;
|
||||
}
|
||||
|
||||
// Move the highlight within the open menu. Arrow keys do not change
|
||||
// readline's text buffer, so no line clearing or rewriting is needed.
|
||||
if (count > 0 && (isUpKey(key) || isDownKey(key))) {
|
||||
const delta = isUpKey(key) ? -1 : 1;
|
||||
setActive((active + delta + count) % count);
|
||||
return;
|
||||
}
|
||||
|
||||
// Complete the highlighted command into both the displayed value and
|
||||
// readline's real edit buffer.
|
||||
if (isTabKey(key)) {
|
||||
if (count === 0) return;
|
||||
const pick = suggestions[active % count];
|
||||
replaceBuffer(rl, pick.name);
|
||||
setValue(pick.name);
|
||||
return; // keep `term` frozen so the menu stays open
|
||||
}
|
||||
|
||||
// Ordinary editing: mirror the readline buffer and refresh the menu.
|
||||
const line = rl.line;
|
||||
setValue(line);
|
||||
setTerm(termFrom(line));
|
||||
setActive(0);
|
||||
});
|
||||
|
||||
const message = theme.style.message(config.message);
|
||||
const formattedValue = status === 'done' ? theme.style.answer(value) : value;
|
||||
|
||||
const lines = suggestions.map((c, i) => {
|
||||
const marker = i === active % count ? chalk.cyan('❯') : ' ';
|
||||
return ` ${chalk.cyan(marker)} ${chalk.bold(c.name.padEnd(7))} ${chalk.dim(c.description)}`;
|
||||
});
|
||||
if (count > 1) {
|
||||
lines.push(chalk.dim(' ↑/↓ choose · Tab complete'));
|
||||
}
|
||||
|
||||
const header = [prefix, message, formattedValue].filter((v) => v !== undefined).join(' ');
|
||||
return [header, lines.length ? lines.join('\n') : undefined];
|
||||
});
|
||||
|
||||
export default autocompleteInput;
|
||||
Reference in New Issue
Block a user