You've already forked uutils.github.io
mirror of
https://github.com/uutils/uutils.github.io.git
synced 2026-06-10 16:12:28 -07:00
Playground: add tab completion for commands and filenames
This commit is contained in:
@@ -579,6 +579,40 @@ function redrawInput() {
|
||||
if (moveBack > 0) terminal.write(`\x1b[${moveBack}D`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute tab-completion result (pure function, no terminal side-effects).
|
||||
* Returns { buffer, cursor, completed, candidates }.
|
||||
*/
|
||||
function tabComplete(buffer, cursor) {
|
||||
const before = buffer.slice(0, cursor);
|
||||
const after = buffer.slice(cursor);
|
||||
const parts = before.split(/\s+/);
|
||||
const word = parts[parts.length - 1];
|
||||
const isCommand = parts.length <= 1;
|
||||
|
||||
const builtins = ["help", "clear", "cd", "locale"];
|
||||
const candidates = isCommand
|
||||
? AVAILABLE_COMMANDS.concat(builtins).filter(c => c.startsWith(word))
|
||||
: Object.keys(SAMPLE_FILES).filter(f => f.startsWith(word));
|
||||
|
||||
if (candidates.length === 1) {
|
||||
const suffix = candidates[0].slice(word.length) + " ";
|
||||
return { buffer: before + suffix + after, cursor: cursor + suffix.length, completed: true, candidates };
|
||||
}
|
||||
if (candidates.length > 1) {
|
||||
let common = candidates[0];
|
||||
for (const c of candidates) {
|
||||
while (!c.startsWith(common)) common = common.slice(0, -1);
|
||||
}
|
||||
if (common.length > word.length) {
|
||||
const suffix = common.slice(word.length);
|
||||
return { buffer: before + suffix + after, cursor: cursor + suffix.length, completed: true, candidates };
|
||||
}
|
||||
return { buffer, cursor, completed: false, candidates };
|
||||
}
|
||||
return { buffer, cursor, completed: false, candidates: [] };
|
||||
}
|
||||
|
||||
async function handleInput(data) {
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const ch = data[i];
|
||||
@@ -662,6 +696,22 @@ async function handleInput(data) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (code === 9) { // Tab — completion
|
||||
const result = tabComplete(inputBuffer, cursorPos);
|
||||
if (result.completed) {
|
||||
inputBuffer = result.buffer;
|
||||
cursorPos = result.cursor;
|
||||
redrawInput();
|
||||
} else if (result.candidates.length > 1) {
|
||||
terminal.write("\r\n" + result.candidates.join(" ") + "\r\n");
|
||||
prompt();
|
||||
terminal.write(inputBuffer);
|
||||
const back = inputBuffer.length - cursorPos;
|
||||
if (back > 0) terminal.write(`\x1b[${back}D`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (code === 21) { // Ctrl+U
|
||||
inputBuffer = "";
|
||||
cursorPos = 0;
|
||||
@@ -806,4 +856,5 @@ window._uutilsTestInternals = {
|
||||
LOCALE_SHORTCUTS,
|
||||
SAMPLE_FILES,
|
||||
AVAILABLE_COMMANDS,
|
||||
tabComplete,
|
||||
};
|
||||
|
||||
@@ -528,6 +528,41 @@ async function runTests() {
|
||||
assert("ja shortcut", T.LOCALE_SHORTCUTS["ja"], "ja-JP");
|
||||
assert("pt shortcut", T.LOCALE_SHORTCUTS["pt"], "pt-BR");
|
||||
|
||||
// ===== Tab completion =====
|
||||
section("Tab completion");
|
||||
|
||||
const tc = T.tabComplete;
|
||||
|
||||
// Single command match
|
||||
let r = tc("fac", 3);
|
||||
assert("complete 'fac' -> 'factor '", r.buffer, "factor ");
|
||||
assert("complete 'fac' completed", r.completed, true);
|
||||
|
||||
// Single filename match
|
||||
r = tc("cat na", 6);
|
||||
assert("complete 'cat na' -> 'cat names.txt '", r.buffer, "cat names.txt ");
|
||||
assert("complete filename completed", r.completed, true);
|
||||
|
||||
// Multiple matches with common prefix
|
||||
r = tc("sha2", 4);
|
||||
assert("complete 'sha2' partial -> 'sha2'", r.buffer.startsWith("sha2"), true);
|
||||
assert("complete 'sha2' has candidates", r.candidates.length > 1, true);
|
||||
|
||||
// No match
|
||||
r = tc("zzzzz", 5);
|
||||
assert("complete 'zzzzz' no match", r.candidates.length, 0);
|
||||
assert("complete 'zzzzz' not completed", r.completed, false);
|
||||
|
||||
// Builtin completion
|
||||
r = tc("hel", 3);
|
||||
assert("complete 'hel' -> 'help '", r.buffer, "help ");
|
||||
|
||||
// Multiple command matches shown
|
||||
r = tc("sh", 2);
|
||||
assert("complete 'sh' multiple candidates", r.candidates.length > 1, true);
|
||||
assert("complete 'sh' includes shuf", r.candidates.includes("shuf"), true);
|
||||
assert("complete 'sh' includes shred", r.candidates.includes("shred"), true);
|
||||
|
||||
// ===== Summary =====
|
||||
const summary = document.getElementById("summary");
|
||||
const total = passed + failed;
|
||||
|
||||
Reference in New Issue
Block a user