From 75923c43d1ceda77ee6e701721033526fdf872e9 Mon Sep 17 00:00:00 2001 From: David Date: Tue, 10 Dec 2019 16:11:31 +0100 Subject: [PATCH] Promtheus: Improve tab completion (#20938) Change in behavior: - no longer suggest everything in empty field, only history - term suggestions need at least one character --- .../prometheus/language_provider.test.ts | 63 ++++++++++++------- .../prometheus/language_provider.ts | 25 ++++++-- 2 files changed, 60 insertions(+), 28 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/language_provider.test.ts b/public/app/plugins/datasource/prometheus/language_provider.test.ts index e7933f47632..1cf3420b997 100644 --- a/public/app/plugins/datasource/prometheus/language_provider.test.ts +++ b/public/app/plugins/datasource/prometheus/language_provider.test.ts @@ -13,35 +13,24 @@ describe('Language completion provider', () => { } as any) as PrometheusDatasource; describe('empty query suggestions', () => { - it('returns default suggestions on empty context', async () => { + it('returns no suggestions on empty context', async () => { const instance = new LanguageProvider(datasource); const value = Plain.deserialize(''); const result = await instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] }); expect(result.context).toBeUndefined(); - expect(result.suggestions).toMatchObject([ - { - label: 'Functions', - }, - ]); + expect(result.suggestions).toMatchObject([]); }); - it('returns default suggestions with metrics on empty context when metrics were provided', async () => { + it('returns no suggestions with metrics on empty context even when metrics were provided', async () => { const instance = new LanguageProvider(datasource); instance.metrics = ['foo', 'bar']; const value = Plain.deserialize(''); const result = await instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] }); expect(result.context).toBeUndefined(); - expect(result.suggestions).toMatchObject([ - { - label: 'Functions', - }, - { - label: 'Metrics', - }, - ]); + expect(result.suggestions).toMatchObject([]); }); - it('returns default suggestions with history on empty context when history was provided', async () => { + it('returns history on empty context when history was provided', async () => { const instance = new LanguageProvider(datasource); const value = Plain.deserialize(''); const history: Array> = [ @@ -65,9 +54,6 @@ describe('Language completion provider', () => { }, ], }, - { - label: 'Functions', - }, ]); }); }); @@ -101,14 +87,31 @@ describe('Language completion provider', () => { }); describe('metric suggestions', () => { - it('returns metrics and function suggestions in an unknown context', async () => { + it('returns history, metrics and function suggestions in an uknown context ', async () => { const instance = new LanguageProvider(datasource); instance.metrics = ['foo', 'bar']; + const history: Array> = [ + { + ts: 0, + query: { refId: '1', expr: 'metric' }, + }, + ]; let value = Plain.deserialize('a'); value = value.setSelection({ anchor: { offset: 1 }, focus: { offset: 1 } }); - const result = await instance.provideCompletionItems({ text: 'a', prefix: 'a', value, wrapperClasses: [] }); + const result = await instance.provideCompletionItems( + { text: 'm', prefix: 'm', value, wrapperClasses: [] }, + { history } + ); expect(result.context).toBeUndefined(); expect(result.suggestions).toMatchObject([ + { + label: 'History', + items: [ + { + label: 'metric', + }, + ], + }, { label: 'Functions', }, @@ -118,12 +121,28 @@ describe('Language completion provider', () => { ]); }); - it('returns metrics and function suggestions after a binary operator', async () => { + it('returns no suggestions directly after a binary operator', async () => { const instance = new LanguageProvider(datasource); instance.metrics = ['foo', 'bar']; const value = Plain.deserialize('*'); const result = await instance.provideCompletionItems({ text: '*', prefix: '', value, wrapperClasses: [] }); expect(result.context).toBeUndefined(); + expect(result.suggestions).toMatchObject([]); + }); + + it('returns metric suggestions with prefix after a binary operator', async () => { + const instance = new LanguageProvider(datasource); + instance.metrics = ['foo', 'bar']; + const value = Plain.deserialize('foo + b'); + const ed = new SlateEditor({ value }); + const valueWithSelection = ed.moveForward(7).value; + const result = await instance.provideCompletionItems({ + text: 'foo + b', + prefix: 'b', + value: valueWithSelection, + wrapperClasses: [], + }); + expect(result.context).toBeUndefined(); expect(result.suggestions).toMatchObject([ { label: 'Functions', diff --git a/public/app/plugins/datasource/prometheus/language_provider.ts b/public/app/plugins/datasource/prometheus/language_provider.ts index c69e2c60b6e..15394c9f8bf 100644 --- a/public/app/plugins/datasource/prometheus/language_provider.ts +++ b/public/app/plugins/datasource/prometheus/language_provider.ts @@ -72,7 +72,14 @@ export default class PromQlLanguageProvider extends LanguageProvider { } // Strip syntax chars - cleanText = (s: string) => s.replace(/[{}[\]="(),!~+\-*/^%]/g, '').trim(); + cleanText = (s: string) => + s + .replace(/[{}[\]="(),!]/g, '') + .replace(/^\s*[~+\-*/^%]/, '') + .trim() + .split(' ') + .pop() + .trim(); get syntax() { return PromqlSyntax; @@ -126,7 +133,7 @@ export default class PromQlLanguageProvider extends LanguageProvider { const noSuffix = !nextCharacter || nextCharacter === ')'; // Empty prefix is safe if it does not immediately follow a complete expression and has no text after it - const safeEmptyPrefix = prefix === '' && !text.match(/^[\]})\s]+$/) && noSuffix; + const safePrefix = prefix && !text.match(/^[\]})\s]+$/) && noSuffix; // About to type next operand if preceded by binary operator const operatorsPattern = /[+\-*/^%]/; @@ -145,7 +152,10 @@ export default class PromQlLanguageProvider extends LanguageProvider { } else if (empty) { // Suggestions for empty query field return this.getEmptyCompletionItems(context); - } else if ((prefixUnrecognized && noSuffix) || safeEmptyPrefix || isNextOperand) { + } else if (prefixUnrecognized && noSuffix && !isNextOperand) { + // Show term suggestions in a couple of scenarios + return this.getBeginningCompletionItems(context); + } else if (prefixUnrecognized && safePrefix) { // Show term suggestions in a couple of scenarios return this.getTermCompletionItems(); } @@ -155,6 +165,12 @@ export default class PromQlLanguageProvider extends LanguageProvider { }; }; + getBeginningCompletionItems = (context: { history: Array> }): TypeaheadOutput => { + return { + suggestions: [...this.getEmptyCompletionItems(context).suggestions, ...this.getTermCompletionItems().suggestions], + }; + }; + getEmptyCompletionItems = (context: { history: Array> }): TypeaheadOutput => { const { history } = context; const suggestions = []; @@ -177,9 +193,6 @@ export default class PromQlLanguageProvider extends LanguageProvider { }); } - const termCompletionItems = this.getTermCompletionItems(); - suggestions.push(...termCompletionItems.suggestions); - return { suggestions }; };