Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Timelion typeahead for argument values #14801

Merged
merged 10 commits into from
Nov 14, 2017
1 change: 1 addition & 0 deletions src/core_plugins/timelion/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export default function (kibana) {
};
},
uses: [
'fieldFormats',
'savedObjectTypes'
]
},
Expand Down
26 changes: 20 additions & 6 deletions src/core_plugins/timelion/public/chain.peg
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
}
}

var currentFunction;
var currentArgs = [];

var functions = [];
var args = [];
var variables = {};
Expand All @@ -40,18 +43,22 @@ arg_list
}

argument
= name:function_name space? '=' space? value:arg_type {
return {
= name:argument_name space? '=' space? value:arg_type {
var arg = {
type: 'namedArg',
name: name,
value: value,
location: simpleLocation(location()),
text: text()
}
};
currentArgs.push(arg);
return arg;
}
/ name:function_name space? '=' {
/ name:argument_name space? '=' {
var exception = {
type: 'incompleteArgument',
currentArgs: currentArgs,
currentFunction: currentFunction,
name: name,
location: simpleLocation(location()),
text: text()
Expand All @@ -71,7 +78,7 @@ arg_type
}

variable_get
= '$' name:function_name {
= '$' name:argument_name {
if (variables[name]) {
return variables[name];
} else {
Expand All @@ -80,7 +87,7 @@ variable_get
}

variable_set
= '$' name:function_name space? '=' space? value:arg_type {
= '$' name:argument_name space? '=' space? value:arg_type {
variables[name] = value;
}

Expand All @@ -97,6 +104,13 @@ series
}

function_name
= first:[a-zA-Z]+ rest:[.a-zA-Z0-9_-]* {
currentFunction = first.join('') + rest.join('');
currentArgs = [];
return currentFunction;
}

argument_name
= first:[a-zA-Z]+ rest:[.a-zA-Z0-9_-]* { return first.join('') + rest.join('') }

function "function"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
SUGGESTION_TYPE,
suggest
} from '../timelion_expression_input_helpers';
import { ArgValueSuggestionsProvider } from '../timelion_expression_suggestions/arg_value_suggestions';

describe('Timelion expression suggestions', () => {

Expand All @@ -15,7 +16,10 @@ describe('Timelion expression suggestions', () => {
args: [
{ name: 'inputSeries' },
{ name: 'argA' },
{ name: 'argAB' }
{
name: 'argAB',
suggestions: [{ name: 'value1' }]
}
]
};
const myFunc2 = {
Expand All @@ -29,6 +33,13 @@ describe('Timelion expression suggestions', () => {
};
const functionList = [func1, myFunc2];
let Parser;
const privateStub = () => {
return {};
};
const indexPatternsStub = {

};
const argValueSuggestions = ArgValueSuggestionsProvider(privateStub, indexPatternsStub); // eslint-disable-line new-cap
beforeEach(function () {
Parser = PEG.buildParser(grammar);
});
Expand All @@ -39,7 +50,7 @@ describe('Timelion expression suggestions', () => {
it('should return function suggestions', async () => {
const expression = '.';
const cursorPosition = 1;
const suggestions = await suggest(expression, functionList, Parser, cursorPosition);
const suggestions = await suggest(expression, functionList, Parser, cursorPosition, argValueSuggestions);
expect(suggestions).to.eql({
'list': [func1, myFunc2],
'location': {
Expand All @@ -52,7 +63,7 @@ describe('Timelion expression suggestions', () => {
it('should filter function suggestions by function name', async () => {
const expression = '.myF';
const cursorPosition = 4;
const suggestions = await suggest(expression, functionList, Parser, cursorPosition);
const suggestions = await suggest(expression, functionList, Parser, cursorPosition, argValueSuggestions);
expect(suggestions).to.eql({
'list': [myFunc2],
'location': {
Expand All @@ -65,15 +76,29 @@ describe('Timelion expression suggestions', () => {
});

describe('incompleteArgument', () => {
it('should return argument value suggestions', async () => {
it('should return no argument value suggestions when not provided by help', async () => {
const expression = '.func1(argA=)';
const cursorPosition = 11;
const suggestions = await suggest(expression, functionList, Parser, cursorPosition);
const suggestions = await suggest(expression, functionList, Parser, cursorPosition, argValueSuggestions);
expect(suggestions).to.eql({
'list': [],
'location': {
'min': 7,
'max': 12
'min': 11,
'max': 11
},
'type': 'argument_value'
});
});

it('should return argument value suggestions when provided by help', async () => {
const expression = '.func1(argAB=)';
const cursorPosition = 11;
const suggestions = await suggest(expression, functionList, Parser, cursorPosition, argValueSuggestions);
expect(suggestions).to.eql({
'list': [{ name: 'value1' }],
'location': {
'min': 11,
'max': 11
},
'type': 'argument_value'
});
Expand All @@ -87,7 +112,7 @@ describe('Timelion expression suggestions', () => {
it('should return function suggestion', async () => {
const expression = '.func1()';
const cursorPosition = 1;
const suggestions = await suggest(expression, functionList, Parser, cursorPosition);
const suggestions = await suggest(expression, functionList, Parser, cursorPosition, argValueSuggestions);
expect(suggestions).to.eql({
'list': [func1],
'location': {
Expand All @@ -104,7 +129,7 @@ describe('Timelion expression suggestions', () => {
it('should return argument suggestions', async () => {
const expression = '.myFunc2()';
const cursorPosition = 9;
const suggestions = await suggest(expression, functionList, Parser, cursorPosition);
const suggestions = await suggest(expression, functionList, Parser, cursorPosition, argValueSuggestions);
expect(suggestions).to.eql({
'list': myFunc2.args,
'location': {
Expand All @@ -117,7 +142,7 @@ describe('Timelion expression suggestions', () => {
it('should not provide argument suggestions for argument that is all ready set in function def', async () => {
const expression = '.myFunc2(argAB=provided,)';
const cursorPosition = 24;
const suggestions = await suggest(expression, functionList, Parser, cursorPosition);
const suggestions = await suggest(expression, functionList, Parser, cursorPosition, argValueSuggestions);
expect(suggestions.type).to.equal(SUGGESTION_TYPE.ARGUMENTS);
expect(suggestions).to.eql({
'list': [{ name: 'argA' }, { name: 'argABC' }],
Expand All @@ -131,7 +156,7 @@ describe('Timelion expression suggestions', () => {
it('should filter argument suggestions by argument name', async () => {
const expression = '.myFunc2(argAB,)';
const cursorPosition = 14;
const suggestions = await suggest(expression, functionList, Parser, cursorPosition);
const suggestions = await suggest(expression, functionList, Parser, cursorPosition, argValueSuggestions);
expect(suggestions).to.eql({
'list': [{ name: 'argAB' }, { name: 'argABC' }],
'location': {
Expand All @@ -144,9 +169,9 @@ describe('Timelion expression suggestions', () => {
it('should not show first argument for chainable functions', async () => {
const expression = '.func1()';
const cursorPosition = 7;
const suggestions = await suggest(expression, functionList, Parser, cursorPosition);
const suggestions = await suggest(expression, functionList, Parser, cursorPosition, argValueSuggestions);
expect(suggestions).to.eql({
'list': [{ name: 'argA' }, { name: 'argAB' }],
'list': [{ name: 'argA' }, { name: 'argAB', suggestions: [{ name: 'value1' }] }],
'location': {
'min': 7,
'max': 7
Expand All @@ -156,10 +181,10 @@ describe('Timelion expression suggestions', () => {
});
});
describe('cursor in argument value', () => {
it('should return argument value suggestions', async () => {
it('should return no argument value suggestions when not provided by help', async () => {
const expression = '.myFunc2(argA=42)';
const cursorPosition = 14;
const suggestions = await suggest(expression, functionList, Parser, cursorPosition);
const suggestions = await suggest(expression, functionList, Parser, cursorPosition, argValueSuggestions);
expect(suggestions).to.eql({
'list': [],
'location': {
Expand All @@ -169,6 +194,20 @@ describe('Timelion expression suggestions', () => {
'type': 'argument_value'
});
});

it('should return no argument value suggestions when provided by help', async () => {
const expression = '.func1(argAB=val)';
const cursorPosition = 16;
const suggestions = await suggest(expression, functionList, Parser, cursorPosition, argValueSuggestions);
expect(suggestions).to.eql({
'list': [{ name: 'value1' }],
'location': {
'min': 13,
'max': 16
},
'type': 'argument_value'
});
});
});
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,12 @@ import {
insertAtLocation,
} from './timelion_expression_input_helpers';
import { comboBoxKeyCodes } from 'ui_framework/services';
import { ArgValueSuggestionsProvider } from './timelion_expression_suggestions/arg_value_suggestions';

const Parser = PEG.buildParser(grammar);
const app = require('ui/modules').get('apps/timelion', []);

app.directive('timelionExpressionInput', function ($document, $http, $interval, $timeout) {
app.directive('timelionExpressionInput', function ($document, $http, $interval, $timeout, Private) {
return {
restrict: 'E',
scope: {
Expand All @@ -51,6 +52,7 @@ app.directive('timelionExpressionInput', function ($document, $http, $interval,
replace: true,
template: timelionExpressionInputTemplate,
link: function (scope, elem) {
const argValueSuggestions = Private(ArgValueSuggestionsProvider);
const expressionInput = elem.find('[data-expression-input]');
const functionReference = {};
let suggestibleFunctionLocation = {};
Expand Down Expand Up @@ -81,35 +83,36 @@ app.directive('timelionExpressionInput', function ($document, $http, $interval,
return;
}

const { min, max } = suggestibleFunctionLocation;
let insertedValue;
let insertPositionMinOffset = 0;

switch (scope.suggestions.type) {
case SUGGESTION_TYPE.FUNCTIONS: {
const functionName = `${scope.suggestions.list[suggestionIndex].name}()`;
const { min, max } = suggestibleFunctionLocation;
// Position the caret inside of the function parentheses.
insertedValue = `${scope.suggestions.list[suggestionIndex].name}()`;

// Update the expression with the function.
// min advanced one to not replace function '.'
const updatedExpression = insertAtLocation(functionName, scope.sheet, min + 1, max);
scope.sheet = updatedExpression;

// Position the caret inside of the function parentheses.
const newCaretOffset = min + functionName.length;
setCaretOffset(newCaretOffset);
insertPositionMinOffset = 1;
break;
}
case SUGGESTION_TYPE.ARGUMENTS: {
const argumentName = `${scope.suggestions.list[suggestionIndex].name}=`;
const { min, max } = suggestibleFunctionLocation;

// Update the expression with the function.
const updatedExpression = insertAtLocation(argumentName, scope.sheet, min, max);
scope.sheet = updatedExpression;

// Position the caret after the '='
const newCaretOffset = min + argumentName.length;
setCaretOffset(newCaretOffset);
insertedValue = `${scope.suggestions.list[suggestionIndex].name}=`;
break;
}
case SUGGESTION_TYPE.ARGUMENT_VALUE: {
// Position the caret after the argument value
insertedValue = `${scope.suggestions.list[suggestionIndex].name}`;
break;
}
}

const updatedExpression = insertAtLocation(insertedValue, scope.sheet, min + insertPositionMinOffset, max);
scope.sheet = updatedExpression;

const newCaretOffset = min + insertedValue.length;
setCaretOffset(newCaretOffset);
}

function scrollToSuggestionAt(index) {
Expand All @@ -127,28 +130,30 @@ app.directive('timelionExpressionInput', function ($document, $http, $interval,
return null;
}

function getSuggestions() {
suggest(
async function getSuggestions() {
const suggestions = await suggest(
scope.sheet,
functionReference.list,
Parser,
getCursorPosition()
).then(suggestions => {
// We're using ES6 Promises, not $q, so we have to wrap this in $apply.
scope.$apply(() => {
getCursorPosition(),
argValueSuggestions
);

// We're using ES6 Promises, not $q, so we have to wrap this in $apply.
scope.$apply(() => {
if (suggestions) {
scope.suggestions.setList(suggestions.list, suggestions.type);
scope.suggestions.show();
suggestibleFunctionLocation = suggestions.location;
$timeout(() => {
const suggestionsList = $('[data-suggestions-list]');
suggestionsList.scrollTop(0);
}, 0);
});
}, (noSuggestions = {}) => {
scope.$apply(() => {
suggestibleFunctionLocation = noSuggestions.location;
scope.suggestions.reset();
});
return;
}

suggestibleFunctionLocation = undefined;
scope.suggestions.reset();
});
}

Expand Down
Loading