mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
* [@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
208 lines
7.8 KiB
JavaScript
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]));
|
|
}
|