From ff0ed06441f5e151a5d314e1513edd3750d56408 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Mon, 12 Nov 2018 17:40:05 +0000 Subject: [PATCH] Explore: Don't suggest term items when text follows Tab completion gets in the way when constructing a query from the inside out: ``` up| => |up => sum(|up) ``` At that point the language provider will not suggest anything. --- .../prometheus/language_provider.ts | 31 +++++-- .../specs/language_provider.test.ts | 89 ++++++++++++++++--- 2 files changed, 101 insertions(+), 19 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/language_provider.ts b/public/app/plugins/datasource/prometheus/language_provider.ts index ac54b08526d..326ab93f2ef 100644 --- a/public/app/plugins/datasource/prometheus/language_provider.ts +++ b/public/app/plugins/datasource/prometheus/language_provider.ts @@ -78,9 +78,16 @@ 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 { // Syntax spans have 3 classes by default. More indicate a recognized token const tokenRecognized = wrapperClasses.length > 3; + + // 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; + // Determine candidates by CSS context if (_.includes(wrapperClasses, 'context-range')) { // Suggestions for metric[|] @@ -90,13 +97,16 @@ export default class PromQlLanguageProvider extends LanguageProvider { return this.getLabelCompletionItems.apply(this, arguments); } else if (_.includes(wrapperClasses, 'context-aggregation')) { return this.getAggregationCompletionItems.apply(this, arguments); + } else if (empty) { + return this.getEmptyCompletionItems(context || {}); } 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 + // Empty prefix, but not directly following a closing brace (e.g., `]|`), or not succeeded by anything except a closing parens, e.g., `sum(|)` + (prefix === '' && !text.match(/^[\]})\s]+$/) && (!nextCharacter || nextCharacter === ')')) || text.match(/[+\-*/^%]/) // Anything after binary operator ) { - return this.getEmptyCompletionItems(context || {}); + return this.getTermCompletionItems(); } return { @@ -106,8 +116,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 +135,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), diff --git a/public/app/plugins/datasource/prometheus/specs/language_provider.test.ts b/public/app/plugins/datasource/prometheus/specs/language_provider.test.ts index 784a8b59739..bcb8cb34082 100644 --- a/public/app/plugins/datasource/prometheus/specs/language_provider.test.ts +++ b/public/app/plugins/datasource/prometheus/specs/language_provider.test.ts @@ -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 result = instance.provideCompletionItems({ text: 'a', prefix: 'a', wrapperClasses: [] }); + 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.length).toEqual(2); + expect(result.suggestions).toMatchObject([ + { + label: 'Functions', + }, + { + label: 'Metrics', + }, + ]); }); - it('returns default suggestions after a binary operator', () => { + it('returns metrics and function suggestions after a binary operator', () => { const instance = new LanguageProvider(datasource, { metrics: ['foo', 'bar'] }); - const result = instance.provideCompletionItems({ text: '*', prefix: '', 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 no suggestions at the beginning of a non-empty function', () => { + const instance = new LanguageProvider(datasource, { metrics: ['foo', 'bar'] }); + 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(0); }); });