Merge pull request #14032 from grafana/davkal/explore-prevent-term-completion

Explore: Don't suggest term items when text follows
pull/14092/head
David 7 years ago committed by GitHub
commit fe45cb9aa1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 44
      public/app/plugins/datasource/prometheus/language_provider.ts
  2. 89
      public/app/plugins/datasource/prometheus/specs/language_provider.test.ts

@ -78,9 +78,24 @@ export default class PromQlLanguageProvider extends LanguageProvider {
};
// Keep this DOM-free for testing
provideCompletionItems({ prefix, wrapperClasses, text }: TypeaheadInput, context?: any): TypeaheadOutput {
provideCompletionItems({ prefix, wrapperClasses, text, value }: TypeaheadInput, context?: any): TypeaheadOutput {
// Local text properties
const empty = value.document.text.length === 0;
const selectedLines = value.document.getTextsAtRangeAsArray(value.selection);
const currentLine = selectedLines.length === 1 ? selectedLines[0] : null;
const nextCharacter = currentLine ? currentLine.text[value.selection.anchorOffset] : null;
// Syntax spans have 3 classes by default. More indicate a recognized token
const tokenRecognized = wrapperClasses.length > 3;
// Non-empty prefix, but not inside known token
const prefixUnrecognized = prefix && !tokenRecognized;
// Prevent suggestions in `function(|suffix)`
const noSuffix = !nextCharacter || nextCharacter === ')';
// Empty prefix is safe if it does not immediately folllow a complete expression and has no text after it
const safeEmptyPrefix = prefix === '' && !text.match(/^[\]})\s]+$/) && noSuffix;
// About to type next operand if preceded by binary operator
const isNextOperand = text.match(/[+\-*/^%]/);
// Determine candidates by CSS context
if (_.includes(wrapperClasses, 'context-range')) {
// Suggestions for metric[|]
@ -89,14 +104,14 @@ export default class PromQlLanguageProvider extends LanguageProvider {
// Suggestions for metric{|} and metric{foo=|}, as well as metric-independent label queries like {|}
return this.getLabelCompletionItems.apply(this, arguments);
} else if (_.includes(wrapperClasses, 'context-aggregation')) {
// Suggestions for sum(metric) by (|)
return this.getAggregationCompletionItems.apply(this, arguments);
} else if (
// Show default suggestions in a couple of scenarios
(prefix && !tokenRecognized) || // Non-empty prefix, but not inside known token
(prefix === '' && !text.match(/^[\]})\s]+$/)) || // Empty prefix, but not following a closing brace
text.match(/[+\-*/^%]/) // Anything after binary operator
) {
} else if (empty) {
// Suggestions for empty query field
return this.getEmptyCompletionItems(context || {});
} else if (prefixUnrecognized || safeEmptyPrefix || isNextOperand) {
// Show term suggestions in a couple of scenarios
return this.getTermCompletionItems();
}
return {
@ -106,8 +121,7 @@ export default class PromQlLanguageProvider extends LanguageProvider {
getEmptyCompletionItems(context: any): TypeaheadOutput {
const { history } = context;
const { metrics } = this;
const suggestions: CompletionItemGroup[] = [];
let suggestions: CompletionItemGroup[] = [];
if (history && history.length > 0) {
const historyItems = _.chain(history)
@ -126,13 +140,23 @@ export default class PromQlLanguageProvider extends LanguageProvider {
});
}
const termCompletionItems = this.getTermCompletionItems();
suggestions = [...suggestions, ...termCompletionItems.suggestions];
return { suggestions };
}
getTermCompletionItems(): TypeaheadOutput {
const { metrics } = this;
const suggestions: CompletionItemGroup[] = [];
suggestions.push({
prefixMatch: true,
label: 'Functions',
items: FUNCTIONS.map(setFunctionKind),
});
if (metrics) {
if (metrics && metrics.length > 0) {
suggestions.push({
label: 'Metrics',
items: metrics.map(wrapLabel),

@ -7,18 +7,47 @@ describe('Language completion provider', () => {
metadataRequest: () => ({ data: { data: [] } }),
};
it('returns default suggestions on emtpty context', () => {
const instance = new LanguageProvider(datasource);
const result = instance.provideCompletionItems({ text: '', prefix: '', wrapperClasses: [] });
expect(result.context).toBeUndefined();
expect(result.refresher).toBeUndefined();
expect(result.suggestions.length).toEqual(2);
describe('empty query suggestions', () => {
it('returns default suggestions on emtpty context', () => {
const instance = new LanguageProvider(datasource);
const value = Plain.deserialize('');
const result = instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] });
expect(result.context).toBeUndefined();
expect(result.refresher).toBeUndefined();
expect(result.suggestions).toMatchObject([
{
label: 'Functions',
},
]);
});
it('returns default suggestions with metrics on emtpty context when metrics were provided', () => {
const instance = new LanguageProvider(datasource, { metrics: ['foo', 'bar'] });
const value = Plain.deserialize('');
const result = instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] });
expect(result.context).toBeUndefined();
expect(result.refresher).toBeUndefined();
expect(result.suggestions).toMatchObject([
{
label: 'Functions',
},
{
label: 'Metrics',
},
]);
});
});
describe('range suggestions', () => {
it('returns range suggestions in range context', () => {
const instance = new LanguageProvider(datasource);
const result = instance.provideCompletionItems({ text: '1', prefix: '1', wrapperClasses: ['context-range'] });
const value = Plain.deserialize('1');
const result = instance.provideCompletionItems({
text: '1',
prefix: '1',
value,
wrapperClasses: ['context-range'],
});
expect(result.context).toBe('context-range');
expect(result.refresher).toBeUndefined();
expect(result.suggestions).toEqual([
@ -31,20 +60,54 @@ describe('Language completion provider', () => {
});
describe('metric suggestions', () => {
it('returns metrics suggestions by default', () => {
it('returns metrics and function suggestions in an unknown context', () => {
const instance = new LanguageProvider(datasource, { metrics: ['foo', 'bar'] });
const value = Plain.deserialize('a');
const result = instance.provideCompletionItems({ text: 'a', prefix: 'a', value, wrapperClasses: [] });
expect(result.context).toBeUndefined();
expect(result.refresher).toBeUndefined();
expect(result.suggestions).toMatchObject([
{
label: 'Functions',
},
{
label: 'Metrics',
},
]);
});
it('returns metrics and function suggestions after a binary operator', () => {
const instance = new LanguageProvider(datasource, { metrics: ['foo', 'bar'] });
const result = instance.provideCompletionItems({ text: 'a', prefix: 'a', wrapperClasses: [] });
const value = Plain.deserialize('*');
const result = instance.provideCompletionItems({ text: '*', prefix: '', value, wrapperClasses: [] });
expect(result.context).toBeUndefined();
expect(result.refresher).toBeUndefined();
expect(result.suggestions.length).toEqual(2);
expect(result.suggestions).toMatchObject([
{
label: 'Functions',
},
{
label: 'Metrics',
},
]);
});
it('returns default suggestions after a binary operator', () => {
it('returns no suggestions at the beginning of a non-empty function', () => {
const instance = new LanguageProvider(datasource, { metrics: ['foo', 'bar'] });
const result = instance.provideCompletionItems({ text: '*', prefix: '', wrapperClasses: [] });
const value = Plain.deserialize('sum(up)');
const range = value.selection.merge({
anchorOffset: 4,
});
const valueWithSelection = value.change().select(range).value;
const result = instance.provideCompletionItems({
text: '',
prefix: '',
value: valueWithSelection,
wrapperClasses: [],
});
expect(result.context).toBeUndefined();
expect(result.refresher).toBeUndefined();
expect(result.suggestions.length).toEqual(2);
expect(result.suggestions.length).toEqual(0);
});
});

Loading…
Cancel
Save