kui-shell
Version:
This is the monorepo for Kui, the hybrid command-line/GUI electron-based Kubernetes tool
588 lines • 26.5 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const debug_1 = require("debug");
const fs_1 = require("fs");
const path_1 = require("path");
const tab_completion_registrar_1 = require("./tab-completion-registrar");
const capabilities_1 = require("@kui-shell/core/api/capabilities");
const REPLUtil = require("@kui-shell/core/api/repl-util");
const UI = require("@kui-shell/core/api/ui-lite");
const LowLevel = require("@kui-shell/core/api/ui-low-level");
const inject_1 = require("@kui-shell/core/api/inject");
const util_1 = require("@kui-shell/core/api/util");
const debug = debug_1.default('plugins/core-support/tab completion');
const listenForUpDown = (prompt) => {
const moveTo = (nextOp, evt) => {
const block = UI.getCurrentBlock();
const temporaryContainer = block && block.querySelector('.tab-completion-temporary');
if (temporaryContainer) {
const current = temporaryContainer.querySelector('.selected');
if (current) {
const next = current[nextOp];
if (next) {
current.classList.remove('selected');
next.classList.add('selected');
next.scrollIntoView();
evt.preventDefault();
}
}
}
};
const previousKeyDown = prompt.onkeydown;
prompt.onkeydown = evt => {
const char = evt.keyCode;
if (char === UI.Keys.Codes.DOWN) {
moveTo('nextSibling', evt);
}
else if (char === UI.Keys.Codes.UP) {
moveTo('previousSibling', evt);
}
else if (char === UI.Keys.Codes.C && evt.ctrlKey) {
LowLevel.doCancel();
}
};
return () => {
prompt.onkeydown = previousKeyDown;
};
};
const listenForEscape = () => {
const previousKeyup = document.onkeyup;
const cleanup = () => {
document.onkeyup = previousKeyup;
};
document.onkeyup = evt => {
if (evt.keyCode === UI.Keys.Codes.ESCAPE) {
const block = UI.getCurrentBlock();
const temporaryContainer = block && block.querySelector('.tab-completion-temporary');
if (temporaryContainer) {
evt.preventDefault();
temporaryContainer.cleanup();
}
}
};
return cleanup;
};
const installKeyHandlers = (prompt) => {
if (prompt) {
return [listenForUpDown(prompt), listenForEscape()];
}
else {
return [];
}
};
const makeCompletionContainer = (block, prompt, partial, dirname, lastIdx) => {
const temporaryContainer = document.createElement('div');
temporaryContainer.className = 'tab-completion-temporary repl-temporary';
const scrollContainer = document.createElement('div');
scrollContainer.className = 'tab-completion-scroll-container';
temporaryContainer.appendChild(scrollContainer);
temporaryContainer.scrollContainer = scrollContainer;
if (!process.env.RUNNING_SHELL_TEST) {
temporaryContainer.classList.add('fade-in');
}
temporaryContainer.partial = partial;
temporaryContainer.dirname = dirname;
temporaryContainer.lastIdx = lastIdx;
temporaryContainer.currentMatches = [];
block.appendChild(temporaryContainer);
const handlers = installKeyHandlers(prompt);
const onChange = () => {
if (!prompt.value.endsWith(partial)) {
return temporaryContainer.cleanup();
}
const args = REPLUtil.split(prompt.value);
const currentText = args[temporaryContainer.lastIdx];
const prevMatches = temporaryContainer.currentMatches;
const newMatches = prevMatches.filter(({ match }) => match.indexOf(currentText) === 0);
const removedMatches = prevMatches.filter(({ match }) => match.indexOf(currentText) !== 0);
temporaryContainer.currentMatches = newMatches;
removedMatches.forEach(({ option }) => temporaryContainer.removeChild(option));
temporaryContainer['partial'] = currentText;
if (temporaryContainer.currentMatches.length === 0) {
temporaryContainer.cleanup();
}
};
prompt.addEventListener('input', onChange);
temporaryContainer.cleanup = () => {
try {
block.removeChild(temporaryContainer);
}
catch (err) {
}
try {
handlers.forEach(cleanup => cleanup());
}
catch (err) {
}
prompt.removeEventListener('input', onChange);
};
setTimeout(() => block.scrollIntoView(), 0);
return temporaryContainer;
};
const shellescape = (str) => {
return str.replace(/ /g, '\\ ');
};
const completeWith = (partial, match, doEscape = false, addSpace = false) => {
const escapedMatch = !doEscape ? match : shellescape(match);
const partialIdx = escapedMatch.indexOf(partial);
const remainder = partialIdx >= 0 ? escapedMatch.substring(partialIdx + partial.length) : escapedMatch;
return remainder + (addSpace ? ' ' : '');
};
const isDirectory = (filepath) => new Promise((resolve, reject) => {
fs_1.lstat(filepath, (err, stats) => {
if (err) {
reject(err);
}
else {
if (stats.isSymbolicLink()) {
debug('following symlink');
return fs_1.realpath(filepath, (err, realpath) => {
if (err) {
reject(err);
}
else {
return isDirectory(realpath)
.then(resolve)
.catch(reject);
}
});
}
resolve(stats.isDirectory());
}
});
});
const complete = (match, prompt, options) => {
const temporaryContainer = options.temporaryContainer;
const partial = options.partial || (temporaryContainer && temporaryContainer.partial);
const dirname = options.dirname || (temporaryContainer && temporaryContainer.dirname);
const doEscape = options.doEscape || false;
const addSpace = options.addSpace || false;
debug('completion', match, partial, dirname);
const completion = completeWith(partial, match, doEscape, addSpace);
if (temporaryContainer) {
temporaryContainer.cleanup();
}
const addToPrompt = (extra) => {
prompt.value = prompt.value + extra;
prompt.scrollLeft = prompt.scrollWidth;
};
if (dirname) {
const filepath = util_1.default.expandHomeDir(path_1.join(dirname, match));
isDirectory(filepath)
.then(isDir => {
if (isDir) {
debug('complete as directory');
addToPrompt(completion + '/');
}
else {
debug('complete as scalar');
addToPrompt(completion);
}
})
.catch(err => {
console.error(err);
});
}
else {
debug('complete as scalar (alt)');
addToPrompt(completion);
}
};
const addSuggestion = (temporaryContainer, prefix, dirname, prompt, doEscape = false) => (match, idx) => {
const matchLabel = match.label || match;
const matchCompletion = match.completion || matchLabel;
const option = document.createElement('div');
const optionInnerFill = document.createElement('span');
const optionInner = document.createElement('a');
const innerPre = document.createElement('span');
const innerPost = document.createElement('span');
optionInner.appendChild(innerPre);
optionInner.appendChild(innerPost);
temporaryContainer.scrollContainer.appendChild(option);
option.appendChild(optionInnerFill);
optionInnerFill.appendChild(optionInner);
optionInnerFill.className = 'tab-completion-temporary-fill';
innerPre.innerText = prefix;
innerPost.innerText = matchLabel.replace(new RegExp(`^${prefix}`), '');
if (match.docs) {
const optionDocs = document.createElement('span');
optionDocs.className = 'deemphasize deemphasize-partial left-pad';
option.appendChild(optionDocs);
optionDocs.innerText = `(${match.docs})`;
}
option.className = 'tab-completion-option';
optionInner.className = 'clickable plain-anchor';
innerPre.classList.add('tab-completion-option-pre');
innerPost.classList.add('tab-completion-option-post');
if (idx === 0) {
option.classList.add('selected');
}
option.addEventListener('click', () => {
complete(matchCompletion, prompt, {
temporaryContainer,
dirname,
doEscape,
addSpace: match.addSpace
});
});
option.setAttribute('data-match', matchLabel);
option.setAttribute('data-completion', matchCompletion);
if (match.addSpace)
option.setAttribute('data-add-space', match.addSpace);
if (doEscape)
option.setAttribute('data-do-escape', 'true');
option.setAttribute('data-value', optionInner.innerText);
temporaryContainer.currentMatches.push({
match: matchLabel,
completion: matchCompletion,
option
});
return { option, optionInner, innerPost };
};
const updateReplToReflectLongestPrefix = (prompt, matches, temporaryContainer, partial = temporaryContainer.partial) => {
if (matches.length > 0) {
const shortest = matches.reduce((minLength, match) => (!minLength ? match.length : Math.min(minLength, match.length)), false);
let idx = 0;
const partialComplete = (idx) => {
const completion = completeWith(partial, matches[0].substring(0, idx), true);
if (completion.length > 0) {
temporaryContainer.partial = completion;
prompt.value = prompt.value + completion;
}
return temporaryContainer.partial;
};
for (idx = 0; idx < shortest; idx++) {
const char = matches[0].charAt(idx);
for (let jdx = 1; jdx < matches.length; jdx++) {
const other = matches[jdx].charAt(idx);
if (char !== other) {
if (idx > 0) {
return partialComplete(idx);
}
else {
return;
}
}
}
}
if (idx > 0) {
return partialComplete(idx);
}
}
};
const presentEnumeratorSuggestions = (block, prompt, temporaryContainer, lastIdx, last) => (filteredList) => {
debug('presentEnumeratorSuggestions', filteredList);
if (filteredList.length === 1) {
complete(filteredList[0], prompt, { partial: last, dirname: false });
}
else if (filteredList.length > 0) {
const partial = last;
const dirname = undefined;
if (!temporaryContainer) {
temporaryContainer = makeCompletionContainer(block, prompt, partial, dirname, lastIdx);
}
updateReplToReflectLongestPrefix(prompt, filteredList, temporaryContainer);
filteredList.forEach(addSuggestion(temporaryContainer, last, dirname, prompt));
}
};
const suggestLocalFile = (last, block, prompt, temporaryContainer, lastIdx) => {
const lastIsDir = last.charAt(last.length - 1) === '/';
const dirname = lastIsDir ? last : path_1.dirname(last);
debug('suggest local file', dirname, last);
if (dirname) {
fs_1.readdir(util_1.default.expandHomeDir(dirname), (err, files) => {
if (err) {
debug('fs.readdir error', err);
}
else {
const partial = path_1.basename(last) + (lastIsDir ? '/' : '');
const matches = files.filter(_f => {
const f = shellescape(_f);
return (lastIsDir || f.indexOf(partial) === 0) && !f.endsWith('~') && f !== '.' && f !== '..';
});
debug('fs.readdir success', partial, matches);
if (matches.length === 1) {
debug('singleton file completion', matches[0]);
complete(matches[0], prompt, {
temporaryContainer,
doEscape: true,
partial,
dirname
});
}
else if (matches.length > 1) {
debug('multi file completion');
if (!temporaryContainer) {
temporaryContainer = makeCompletionContainer(block, prompt, partial, dirname, lastIdx);
}
updateReplToReflectLongestPrefix(prompt, matches, temporaryContainer);
matches.forEach((match, idx) => {
const { option, optionInner, innerPost } = addSuggestion(temporaryContainer, '', dirname, prompt, true)(match, idx);
const filepath = path_1.join(dirname, match);
isDirectory(filepath)
.then(isDir => {
if (isDir) {
innerPost.innerText = innerPost.innerText + '/';
}
option.setAttribute('data-value', optionInner.innerText);
})
.catch(err => {
console.error(err);
});
});
}
}
});
}
};
const filterAndPresentEntitySuggestions = (last, block, prompt, temporaryContainer, lastIdx) => entities => {
debug('filtering these entities', entities);
debug('against this filter', last);
const filteredList = entities
.map(({ name, packageName, namespace }) => {
const packageNamePart = packageName ? `${packageName}/` : '';
const actionWithPackage = `${packageNamePart}${name}`;
const fqn = `/${namespace}/${actionWithPackage}`;
return ((name.indexOf(last) === 0 && actionWithPackage) ||
(actionWithPackage.indexOf(last) === 0 && actionWithPackage) ||
(fqn.indexOf(last) === 0 && fqn));
})
.filter(x => x);
debug('filtered list', filteredList);
if (filteredList.length === 1) {
debug('singleton entity match', filteredList[0]);
complete(filteredList[0], prompt, { partial: last, dirname: false });
}
else if (filteredList.length > 0) {
const partial = last;
const dirname = undefined;
if (!temporaryContainer) {
temporaryContainer = makeCompletionContainer(block, prompt, partial, dirname, lastIdx);
}
updateReplToReflectLongestPrefix(prompt, filteredList, temporaryContainer);
filteredList.forEach(addSuggestion(temporaryContainer, last, dirname, prompt));
}
};
const suggestCommandCompletions = (_matches, partial, block, prompt, temporaryContainer) => {
const matches = _matches
.filter(({ usage, docs }) => usage || docs)
.map(({ command, docs, usage = {
command,
docs,
commandPrefix: undefined,
title: undefined,
header: undefined
} }) => ({
label: command,
completion: command,
addSpace: true,
docs: usage.title || usage.header || usage.docs
}));
if (matches.length === 1) {
debug('singleton command completion', matches[0]);
complete(matches[0].completion, prompt, { partial, dirname: false });
}
else if (matches.length > 0) {
debug('suggesting command completions', matches, partial);
if (!temporaryContainer) {
temporaryContainer = makeCompletionContainer(block, prompt, partial);
}
matches.forEach(addSuggestion(temporaryContainer, partial, undefined, prompt));
}
};
const suggest = (param, last, block, prompt, temporaryContainer, lastIdx) => {
if (param.file) {
return suggestLocalFile(last, block, prompt, temporaryContainer, lastIdx);
}
else if (param.entity) {
const tab = UI.getTabFromTarget(block);
return tab.REPL.qexec(`${param.entity} list --limit 200`)
.then(response => response.body)
.then(filterAndPresentEntitySuggestions(path_1.basename(last), block, prompt, temporaryContainer, lastIdx));
}
};
exports.default = () => {
if (typeof document === 'undefined')
return;
if (capabilities_1.default.inBrowser()) {
inject_1.injectCSS({
css: require('@kui-shell/plugin-core-support/web/css/tab-completion.css'),
key: 'tab-completion.css'
});
}
else {
const root = path_1.dirname(require.resolve('@kui-shell/plugin-core-support/package.json'));
inject_1.injectCSS(path_1.join(root, 'web/css/tab-completion.css'));
}
let currentEnumeratorAsync;
document.addEventListener('keydown', (evt) => __awaiter(void 0, void 0, void 0, function* () {
const block = UI.getCurrentBlock();
const temporaryContainer = block && block.querySelector('.tab-completion-temporary');
if (evt.keyCode === UI.Keys.Codes.ENTER) {
if (temporaryContainer) {
const current = temporaryContainer.querySelector('.selected');
if (current) {
const completion = current.getAttribute('data-completion');
const doEscape = current.hasAttribute('data-do-escape');
const addSpace = current.hasAttribute('data-add-space');
const prompt = UI.getCurrentPrompt();
complete(completion, prompt, {
temporaryContainer,
doEscape,
addSpace
});
}
evt.preventDefault();
try {
temporaryContainer.cleanup();
}
catch (err) {
}
}
}
else if (evt.keyCode === UI.Keys.Codes.TAB) {
const prompt = UI.getCurrentPrompt();
if (prompt) {
if (LowLevel.isUsingCustomPrompt(prompt)) {
evt.preventDefault();
return true;
}
const value = prompt.value;
if (value) {
evt.preventDefault();
if (temporaryContainer) {
const current = temporaryContainer.querySelector('.selected');
const next = (current.nextSibling ||
temporaryContainer.querySelector('.tab-completion-option:first-child'));
if (next) {
current.classList.remove('selected');
next.classList.add('selected');
next.scrollIntoView();
}
return;
}
const handleUsage = (usageError) => {
const usage = usageError.raw ? usageError.raw.usage || usageError.raw : usageError.usage || usageError;
debug('usage', usage, usageError);
if (usage.fn) {
debug('resolving generator');
handleUsage(usage.fn(usage.command));
}
else if (usageError.partialMatches || usageError.available) {
suggestCommandCompletions(usageError.partialMatches || usageError.available, prompt.value, block, prompt, temporaryContainer);
}
else if (usage && usage.command) {
const required = usage.required || [];
const optionalPositionals = (usage.optional || []).filter(({ positional }) => positional);
const oneofs = usage.oneof ? [usage.oneof[0]] : [];
const positionals = required.concat(oneofs).concat(optionalPositionals);
debug('positionals', positionals);
if (positionals.length > 0) {
const args = REPLUtil.split(prompt.value).filter(_ => !/^-/.test(_));
const commandIdx = args.indexOf(usage.command);
const nActuals = args.length - commandIdx - 1;
const lastIdx = Math.max(0, nActuals - 1);
const param = positionals[lastIdx];
debug('maybe', args, commandIdx, lastIdx, nActuals, param, args[commandIdx + lastIdx]);
if (commandIdx === args.length - 1 && !prompt.value.match(/\s+$/)) {
}
else if (param) {
try {
suggest(param, util_1.default.findFile(args[commandIdx + lastIdx + 1], { safe: true }), block, prompt, temporaryContainer, commandIdx + lastIdx);
}
catch (err) {
console.error(err);
}
}
}
}
else if (!capabilities_1.default.inBrowser()) {
const { A: args, endIndices } = REPLUtil._split(prompt.value, true, true);
const lastIdx = prompt.selectionStart;
debug('falling back on local file completion', args, lastIdx);
for (let ii = 0; ii < endIndices.length; ii++) {
if (endIndices[ii] >= lastIdx) {
const last = prompt.value.substring(endIndices[ii - 1], lastIdx).replace(/^\s+/, '');
suggestLocalFile(last, block, prompt, temporaryContainer, lastIdx);
break;
}
}
}
};
const lastIdx = prompt.selectionStart;
const { A: argv, endIndices } = REPLUtil._split(prompt.value, true, true);
const minimist = yield Promise.resolve().then(() => require('yargs-parser'));
const options = minimist(argv);
const toBeCompletedIdx = endIndices.findIndex(idx => idx >= lastIdx);
const completingTrailingEmpty = lastIdx > endIndices[endIndices.length - 1];
if (toBeCompletedIdx >= 0 || completingTrailingEmpty) {
const last = completingTrailingEmpty
? ''
: prompt.value.substring(endIndices[toBeCompletedIdx - 1], lastIdx).replace(/^\s+/, '');
const argvNoOptions = options._;
delete options._;
const commandLine = {
command: prompt.value,
argv,
argvNoOptions,
parsedOptions: options
};
const spec = {
toBeCompletedIdx,
toBeCompleted: last
};
const gotSomeCompletions = yield new Promise(resolve => {
if (currentEnumeratorAsync) {
clearTimeout(currentEnumeratorAsync);
}
const myEnumeratorAsync = setTimeout(() => __awaiter(void 0, void 0, void 0, function* () {
const completions = yield tab_completion_registrar_1.applyEnumerator(commandLine, spec);
if (myEnumeratorAsync !== currentEnumeratorAsync) {
return;
}
if (completions && completions.length > 0) {
presentEnumeratorSuggestions(block, prompt, temporaryContainer, lastIdx, last)(completions);
currentEnumeratorAsync = undefined;
resolve(true);
}
else {
resolve(false);
}
}));
currentEnumeratorAsync = myEnumeratorAsync;
});
if (gotSomeCompletions) {
return;
}
}
try {
debug('fetching usage', value);
const tab = UI.getTabFromTarget(block);
const usage = tab.REPL.qexec(`${value} --help`, undefined, undefined, {
failWithUsage: true
});
if (usage.then) {
usage.then(handleUsage, handleUsage);
}
else {
handleUsage(usage);
}
}
catch (err) {
console.error(err);
}
}
}
}
}));
};
//# sourceMappingURL=tab-completion.js.map