Browse Source

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

Explore: Don't suggest term items when text follows
David 7 years ago
parent
commit
fe45cb9aa1

+ 34 - 10
public/app/plugins/datasource/prometheus/language_provider.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),

+ 76 - 13
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 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);
     });
   });