kibana/x-pack/plugins/canvas/common/lib/autocomplete.js
Spencer e618cf43da
[6.x] [@kbn/interpreter] improve build/packaging (#26096) (#26136)
* [@kbn/interpreter] improve build/packaging (#26096)

Summary of changes:

 - move all build artifacts under `target` directory
 - run babel and webpack in parallel
 - support optional watch and sourcemaps in build
 - expose /common /public /plugin /server sub-exports as index.js
 - avoid importing deeply from `@kbn/interpreter`
 - move a couple missed dependencies from x-pack to kibana
 - remove custom babel-register implementation

* fix bad conflict resolution
2018-11-24 12:26:07 -07:00

208 lines
7.8 KiB
JavaScript

/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { uniq } from 'lodash';
import { parse, getByAlias } from '@kbn/interpreter/common';
const MARKER = 'CANVAS_SUGGESTION_MARKER';
/**
* Generates the AST with the given expression and then returns the function and argument definitions
* at the given position in the expression, if there are any.
*/
export function getFnArgDefAtPosition(specs, expression, position) {
const text = expression.substr(0, position) + MARKER + expression.substr(position);
try {
const ast = parse(text, { addMeta: true });
const { ast: newAst, fnIndex, argName } = getFnArgAtPosition(ast, position);
const fn = newAst.node.chain[fnIndex].node;
const fnDef = getByAlias(specs, fn.function.replace(MARKER, ''));
if (fnDef && argName) {
const argDef = getByAlias(fnDef.args, argName);
return { fnDef, argDef };
}
return { fnDef };
} catch (e) {
// Fail silently
}
return [];
}
/**
* Gets a list of suggestions for the given expression at the given position. It does this by
* inserting a marker at the given position, then parsing the resulting expression. This way we can
* see what the marker would turn into, which tells us what sorts of things to suggest. For
* example, if the marker turns into a function name, then we suggest functions. If it turns into
* an unnamed argument, we suggest argument names. If it turns into a value, we suggest values.
*/
export function getAutocompleteSuggestions(specs, expression, position) {
const text = expression.substr(0, position) + MARKER + expression.substr(position);
try {
const ast = parse(text, { addMeta: true });
const { ast: newAst, fnIndex, argName, argIndex } = getFnArgAtPosition(ast, position);
const fn = newAst.node.chain[fnIndex].node;
if (fn.function.includes(MARKER)) return getFnNameSuggestions(specs, newAst, fnIndex);
if (argName === '_') return getArgNameSuggestions(specs, newAst, fnIndex, argName, argIndex);
if (argName) return getArgValueSuggestions(specs, newAst, fnIndex, argName, argIndex);
} catch (e) {
// Fail silently
}
return [];
}
/**
* Get the function and argument (if there is one) at the given position.
*/
function getFnArgAtPosition(ast, position) {
const fnIndex = ast.node.chain.findIndex(fn => fn.start <= position && position <= fn.end);
const fn = ast.node.chain[fnIndex];
for (const [argName, argValues] of Object.entries(fn.node.arguments)) {
for (let argIndex = 0; argIndex < argValues.length; argIndex++) {
const value = argValues[argIndex];
if (value.start <= position && position <= value.end) {
if (value.node !== null && value.node.type === 'expression')
return getFnArgAtPosition(value, position);
return { ast, fnIndex, argName, argIndex };
}
}
}
return { ast, fnIndex };
}
function getFnNameSuggestions(specs, ast, fnIndex) {
// Filter the list of functions by the text at the marker
const { start, end, node: fn } = ast.node.chain[fnIndex];
const query = fn.function.replace(MARKER, '');
const matchingFnDefs = specs.filter(({ name }) => textMatches(name, query));
// Sort by whether or not the function expects the previous function's return type, then by
// whether or not the function name starts with the text at the marker, then alphabetically
const prevFn = ast.node.chain[fnIndex - 1];
const prevFnDef = prevFn && getByAlias(specs, prevFn.node.function);
const prevFnType = prevFnDef && prevFnDef.type;
const comparator = combinedComparator(
prevFnTypeComparator(prevFnType),
invokeWithProp(startsWithComparator(query), 'name'),
invokeWithProp(alphanumericalComparator, 'name')
);
const fnDefs = matchingFnDefs.sort(comparator);
return fnDefs.map(fnDef => {
return { type: 'function', text: fnDef.name + ' ', start, end: end - MARKER.length, fnDef };
});
}
function getArgNameSuggestions(specs, ast, fnIndex, argName, argIndex) {
// Get the list of args from the function definition
const fn = ast.node.chain[fnIndex].node;
const fnDef = getByAlias(specs, fn.function);
if (!fnDef) return [];
// We use the exact text instead of the value because it is always a string and might be quoted
const { text, start, end } = fn.arguments[argName][argIndex];
// Filter the list of args by the text at the marker
const query = text.replace(MARKER, '');
const matchingArgDefs = Object.values(fnDef.args).filter(({ name }) => textMatches(name, query));
// Filter the list of args by those which aren't already present (unless they allow multi)
const argEntries = Object.entries(fn.arguments).map(([name, values]) => {
return [name, values.filter(value => !value.text.includes(MARKER))];
});
const unusedArgDefs = matchingArgDefs.filter(argDef => {
if (argDef.multi) return true;
return !argEntries.some(([name, values]) => {
return values.length && (name === argDef.name || argDef.aliases.includes(name));
});
});
// Sort by whether or not the arg is also the unnamed, then by whether or not the arg name starts
// with the text at the marker, then alphabetically
const comparator = combinedComparator(
unnamedArgComparator,
invokeWithProp(startsWithComparator(query), 'name'),
invokeWithProp(alphanumericalComparator, 'name')
);
const argDefs = unusedArgDefs.sort(comparator);
return argDefs.map(argDef => {
return { type: 'argument', text: argDef.name + '=', start, end: end - MARKER.length, argDef };
});
}
function getArgValueSuggestions(specs, ast, fnIndex, argName, argIndex) {
// Get the list of values from the argument definition
const fn = ast.node.chain[fnIndex].node;
const fnDef = getByAlias(specs, fn.function);
if (!fnDef) return [];
const argDef = getByAlias(fnDef.args, argName);
if (!argDef) return [];
// Get suggestions from the argument definition, including the default
const { start, end, node } = fn.arguments[argName][argIndex];
const query = node.replace(MARKER, '');
const suggestions = uniq(argDef.options.concat(argDef.default || []));
// Filter the list of suggestions by the text at the marker
const filtered = suggestions.filter(option => textMatches(String(option), query));
// Sort by whether or not the value starts with the text at the marker, then alphabetically
const comparator = combinedComparator(startsWithComparator(query), alphanumericalComparator);
const sorted = filtered.sort(comparator);
return sorted.map(value => {
const text = maybeQuote(value) + ' ';
return { start, end: end - MARKER.length, type: 'value', text };
});
}
function textMatches(text, query) {
return text.toLowerCase().includes(query.toLowerCase().trim());
}
function maybeQuote(value) {
if (typeof value === 'string') {
if (value.match(/^\{.*\}$/)) return value;
return `"${value.replace(/"/g, '\\"')}"`;
}
return value;
}
function prevFnTypeComparator(prevFnType) {
return (a, b) =>
Boolean(b.context.types && b.context.types.includes(prevFnType)) -
Boolean(a.context.types && a.context.types.includes(prevFnType));
}
function unnamedArgComparator(a, b) {
return b.aliases.includes('_') - a.aliases.includes('_');
}
function alphanumericalComparator(a, b) {
if (a < b) return -1;
if (a > b) return 1;
return 0;
}
function startsWithComparator(query) {
return (a, b) => String(b).startsWith(query) - String(a).startsWith(query);
}
function combinedComparator(...comparators) {
return (a, b) =>
comparators.reduce((acc, comparator) => {
if (acc !== 0) return acc;
return comparator(a, b);
}, 0);
}
function invokeWithProp(fn, prop) {
return (...args) => fn(...args.map(arg => arg[prop]));
}