From a61c8d23d4b1b393d869a667d0a4652b4c32c1f6 Mon Sep 17 00:00:00 2001
From: David Kaltschmidt <david.kaltschmidt@gmail.com>
Date: Fri, 30 Nov 2018 15:13:53 +0100
Subject: [PATCH] Explore: Fix label and history suggestions

- fork promql's tokenizer (need to specify that labels context can only follow beginning of line or whitespace)
- remove unneeded syntax features
- only present history items when field is empty
---
 .../logging/language_provider.test.ts         | 31 +++++++++++++++++--
 .../datasource/logging/language_provider.ts   | 19 +++++-------
 .../app/plugins/datasource/logging/syntax.ts  | 28 +++++++++++++++++
 3 files changed, 64 insertions(+), 14 deletions(-)
 create mode 100644 public/app/plugins/datasource/logging/syntax.ts

diff --git a/public/app/plugins/datasource/logging/language_provider.test.ts b/public/app/plugins/datasource/logging/language_provider.test.ts
index f4fc7efa7cb..8c2c805940f 100644
--- a/public/app/plugins/datasource/logging/language_provider.test.ts
+++ b/public/app/plugins/datasource/logging/language_provider.test.ts
@@ -8,9 +8,10 @@ describe('Language completion provider', () => {
   };
 
   describe('empty query suggestions', () => {
-    it('returns default suggestions on emtpty context', () => {
+    it('returns no suggestions on emtpty context', () => {
       const instance = new LanguageProvider(datasource);
-      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(0);
@@ -38,6 +39,32 @@ describe('Language completion provider', () => {
         },
       ]);
     });
+
+    it('returns no suggestions within regexp', () => {
+      const instance = new LanguageProvider(datasource);
+      const value = Plain.deserialize('{} ()');
+      const range = value.selection.merge({
+        anchorOffset: 4,
+      });
+      const valueWithSelection = value.change().select(range).value;
+      const history = [
+        {
+          query: { refId: '1', expr: '{app="foo"}' },
+        },
+      ];
+      const result = instance.provideCompletionItems(
+        {
+          text: '',
+          prefix: '',
+          value: valueWithSelection,
+          wrapperClasses: [],
+        },
+        { history }
+      );
+      expect(result.context).toBeUndefined();
+      expect(result.refresher).toBeUndefined();
+      expect(result.suggestions.length).toEqual(0);
+    });
   });
 
   describe('label suggestions', () => {
diff --git a/public/app/plugins/datasource/logging/language_provider.ts b/public/app/plugins/datasource/logging/language_provider.ts
index 21d2846ac63..a992084159a 100644
--- a/public/app/plugins/datasource/logging/language_provider.ts
+++ b/public/app/plugins/datasource/logging/language_provider.ts
@@ -10,7 +10,7 @@ import {
   HistoryItem,
 } from 'app/types/explore';
 import { parseSelector, labelRegexp, selectorRegexp } from 'app/plugins/datasource/prometheus/language_utils';
-import PromqlSyntax from 'app/plugins/datasource/prometheus/promql';
+import syntax from './syntax';
 import { DataQuery } from 'app/types';
 
 const DEFAULT_KEYS = ['job', 'namespace'];
@@ -55,7 +55,7 @@ export default class LoggingLanguageProvider extends LanguageProvider {
   cleanText = s => s.replace(/[{}[\]="(),!~+\-*/^%]/g, '').trim();
 
   getSyntax() {
-    return PromqlSyntax;
+    return syntax;
   }
 
   request = url => {
@@ -70,19 +70,14 @@ export default class LoggingLanguageProvider extends LanguageProvider {
   };
 
   // Keep this DOM-free for testing
-  provideCompletionItems({ prefix, wrapperClasses, text }: TypeaheadInput, context?: any): TypeaheadOutput {
-    // Syntax spans have 3 classes by default. More indicate a recognized token
-    const tokenRecognized = wrapperClasses.length > 3;
+  provideCompletionItems({ prefix, wrapperClasses, text, value }: TypeaheadInput, context?: any): TypeaheadOutput {
+    // Local text properties
+    const empty = value.document.text.length === 0;
     // Determine candidates by CSS context
     if (_.includes(wrapperClasses, 'context-labels')) {
-      // Suggestions for metric{|} and metric{foo=|}, as well as metric-independent label queries like {|}
+      // Suggestions for {|} and {foo=|}
       return this.getLabelCompletionItems.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) {
       return this.getEmptyCompletionItems(context || {});
     }
 
diff --git a/public/app/plugins/datasource/logging/syntax.ts b/public/app/plugins/datasource/logging/syntax.ts
new file mode 100644
index 00000000000..aca7d09ef4d
--- /dev/null
+++ b/public/app/plugins/datasource/logging/syntax.ts
@@ -0,0 +1,28 @@
+/* tslint:disable max-line-length */
+
+const tokenizer = {
+  comment: {
+    pattern: /(^|[^\n])#.*/,
+    lookbehind: true,
+  },
+  'context-labels': {
+    pattern: /(^|\s)\{[^}]*(?=})/,
+    lookbehind: true,
+    inside: {
+      'label-key': {
+        pattern: /[a-z_]\w*(?=\s*(=|!=|=~|!~))/,
+        alias: 'attr-name',
+      },
+      'label-value': {
+        pattern: /"(?:\\.|[^\\"])*"/,
+        greedy: true,
+        alias: 'attr-value',
+      },
+    },
+  },
+  // number: /\b-?\d+((\.\d*)?([eE][+-]?\d+)?)?\b/,
+  operator: new RegExp(`/&&?|\\|?\\||!=?|<(?:=>?|<|>)?|>[>=]?`, 'i'),
+  punctuation: /[{}`,.]/,
+};
+
+export default tokenizer;