Browse Source

Revert "Chore: Update Slate to 0.47.8 (#18412)" (#19167)

This reverts commit 601853fc8453f73fb5df4bd0fe8c2a228aed2d3d.
Dominik Prokop 6 years ago
parent
commit
503dccb771
51 changed files with 1318 additions and 1705 deletions
  1. 5 6
      package.json
  2. 1 1
      packages/grafana-toolkit/src/config/webpack.plugin.config.ts
  3. 0 5
      packages/grafana-ui/package.json
  4. 3 4
      packages/grafana-ui/rollup.config.ts
  5. 22 24
      packages/grafana-ui/src/components/DataLinks/DataLinkInput.tsx
  6. 0 1
      packages/grafana-ui/src/index.ts
  7. 0 1
      packages/grafana-ui/src/slate-plugins/index.ts
  8. 0 3
      packages/grafana-ui/src/slate-plugins/slate-prism/TOKEN_MARK.ts
  9. 0 160
      packages/grafana-ui/src/slate-plugins/slate-prism/index.ts
  10. 0 77
      packages/grafana-ui/src/slate-plugins/slate-prism/options.tsx
  11. 14 13
      packages/grafana-ui/src/utils/slate.ts
  12. 0 4
      packages/grafana-ui/tsconfig.json
  13. 41 0
      public/app/features/explore/QueryField.test.tsx
  14. 557 50
      public/app/features/explore/QueryField.tsx
  15. 72 152
      public/app/features/explore/Typeahead.tsx
  16. 18 7
      public/app/features/explore/TypeaheadInfo.tsx
  17. 17 19
      public/app/features/explore/TypeaheadItem.tsx
  18. 39 0
      public/app/features/explore/slate-plugins/braces.test.ts
  19. 0 40
      public/app/features/explore/slate-plugins/braces.test.tsx
  20. 18 22
      public/app/features/explore/slate-plugins/braces.ts
  21. 39 0
      public/app/features/explore/slate-plugins/clear.test.ts
  22. 0 42
      public/app/features/explore/slate-plugins/clear.test.tsx
  23. 8 13
      public/app/features/explore/slate-plugins/clear.ts
  24. 0 61
      public/app/features/explore/slate-plugins/clipboard.ts
  25. 0 93
      public/app/features/explore/slate-plugins/indentation.ts
  26. 9 12
      public/app/features/explore/slate-plugins/newline.ts
  27. 0 17
      public/app/features/explore/slate-plugins/runner.test.tsx
  28. 2 5
      public/app/features/explore/slate-plugins/runner.ts
  29. 0 72
      public/app/features/explore/slate-plugins/selection_shortcuts.ts
  30. 0 313
      public/app/features/explore/slate-plugins/suggestions.tsx
  31. 5 3
      public/app/features/explore/utils/typeahead.ts
  32. 2 2
      public/app/features/plugins/plugin_loader.ts
  33. 5 3
      public/app/plugins/datasource/elasticsearch/components/ElasticsearchQueryField.tsx
  34. 8 9
      public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx
  35. 36 23
      public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/query_field.tsx
  36. 1 2
      public/app/plugins/datasource/loki/components/LokiQueryField.tsx
  37. 23 26
      public/app/plugins/datasource/loki/components/LokiQueryFieldForm.tsx
  38. 0 1
      public/app/plugins/datasource/loki/components/useLokiSyntax.test.ts
  39. 1 0
      public/app/plugins/datasource/loki/components/useLokiSyntax.ts
  40. 25 32
      public/app/plugins/datasource/loki/language_provider.test.ts
  41. 29 25
      public/app/plugins/datasource/loki/language_provider.ts
  42. 2 2
      public/app/plugins/datasource/loki/mocks.ts
  43. 1 3
      public/app/plugins/datasource/loki/syntax.ts
  44. 19 15
      public/app/plugins/datasource/prometheus/components/PromQueryField.tsx
  45. 60 85
      public/app/plugins/datasource/prometheus/language_provider.ts
  46. 4 4
      public/app/plugins/datasource/prometheus/language_utils.ts
  47. 111 111
      public/app/plugins/datasource/prometheus/specs/language_provider.test.ts
  48. 7 25
      public/app/types/explore.ts
  49. 4 4
      public/sass/components/_slate_editor.scss
  50. 1 2
      tsconfig.json
  51. 109 111
      yarn.lock

+ 5 - 6
package.json

@@ -52,9 +52,7 @@
     "@types/redux-logger": "3.0.7",
     "@types/redux-mock-store": "1.0.1",
     "@types/reselect": "2.2.0",
-    "@types/slate": "0.47.1",
-    "@types/slate-plain-serializer": "0.6.1",
-    "@types/slate-react": "0.22.5",
+    "@types/slate": "0.44.11",
     "@types/tinycolor2": "1.4.2",
     "angular-mocks": "1.6.6",
     "autoprefixer": "9.5.0",
@@ -195,7 +193,6 @@
   },
   "dependencies": {
     "@babel/polyfill": "7.2.5",
-    "@grafana/slate-react": "0.22.9-grafana",
     "@torkelo/react-select": "2.4.1",
     "angular": "1.6.6",
     "angular-bindonce": "0.3.1",
@@ -246,8 +243,10 @@
     "rst2html": "github:thoward/rst2html#990cb89",
     "rxjs": "6.4.0",
     "search-query-parser": "1.5.2",
-    "slate": "0.47.8",
-    "slate-plain-serializer": "0.7.10",
+    "slate": "0.33.8",
+    "slate-plain-serializer": "0.5.41",
+    "slate-prism": "0.5.0",
+    "slate-react": "0.12.11",
     "tether": "1.4.5",
     "tether-drop": "https://github.com/torkelo/drop/tarball/master",
     "tinycolor2": "1.4.1",

+ 1 - 1
packages/grafana-toolkit/src/config/webpack.plugin.config.ts

@@ -149,7 +149,7 @@ export const getWebpackConfig: WebpackConfigurationGetter = options => {
       'emotion',
       'prismjs',
       'slate-plain-serializer',
-      '@grafana/slate-react',
+      'slate-react',
       'react',
       'react-dom',
       'react-redux',

+ 0 - 5
packages/grafana-ui/package.json

@@ -26,12 +26,10 @@
   },
   "dependencies": {
     "@grafana/data": "^6.4.0-alpha",
-    "@grafana/slate-react": "0.22.9-grafana",
     "@torkelo/react-select": "2.1.1",
     "@types/react-color": "2.17.0",
     "classnames": "2.2.6",
     "d3": "5.9.1",
-    "immutable": "3.8.2",
     "jquery": "3.4.1",
     "lodash": "4.17.15",
     "moment": "2.24.0",
@@ -47,7 +45,6 @@
     "react-storybook-addon-props-combinations": "1.1.0",
     "react-transition-group": "2.6.1",
     "react-virtualized": "9.21.0",
-    "slate": "0.47.8",
     "tinycolor2": "1.4.1"
   },
   "devDependencies": {
@@ -68,8 +65,6 @@
     "@types/react-custom-scrollbars": "4.0.5",
     "@types/react-test-renderer": "16.8.1",
     "@types/react-transition-group": "2.0.16",
-    "@types/slate": "0.47.1",
-    "@types/slate-react": "0.22.5",
     "@types/storybook__addon-actions": "3.4.2",
     "@types/storybook__addon-info": "4.1.1",
     "@types/storybook__addon-knobs": "4.0.4",

+ 3 - 4
packages/grafana-ui/rollup.config.ts

@@ -1,6 +1,6 @@
 import resolve from 'rollup-plugin-node-resolve';
 import commonjs from 'rollup-plugin-commonjs';
-// import sourceMaps from 'rollup-plugin-sourcemaps';
+import sourceMaps from 'rollup-plugin-sourcemaps';
 import { terser } from 'rollup-plugin-terser';
 
 const pkg = require('./package.json');
@@ -47,20 +47,19 @@ const buildCjsPackage = ({ env }) => {
           ],
           '../../node_modules/react-color/lib/components/common': ['Saturation', 'Hue', 'Alpha'],
           '../../node_modules/immutable/dist/immutable.js': [
-            'Record',
             'Set',
             'Map',
             'List',
             'OrderedSet',
             'is',
             'Stack',
+            'Record',
           ],
-          'node_modules/immutable/dist/immutable.js': ['Record', 'Set', 'Map', 'List', 'OrderedSet', 'is', 'Stack'],
           '../../node_modules/esrever/esrever.js': ['reverse'],
         },
       }),
       resolve(),
-      // sourceMaps(),
+      sourceMaps(),
       env === 'production' && terser(),
     ],
   };

+ 22 - 24
packages/grafana-ui/src/components/DataLinks/DataLinkInput.tsx

@@ -1,16 +1,19 @@
 import React, { useState, useMemo, useCallback, useContext } from 'react';
 import { VariableSuggestion, VariableOrigin, DataLinkSuggestions } from './DataLinkSuggestions';
-import { makeValue, ThemeContext, DataLinkBuiltInVars, SCHEMA } from '../../index';
+import { makeValue, ThemeContext, DataLinkBuiltInVars } from '../../index';
 import { SelectionReference } from './SelectionReference';
 import { Portal } from '../index';
-import { Editor } from '@grafana/slate-react';
-import { Value, Editor as CoreEditor } from 'slate';
+// @ts-ignore
+import { Editor } from 'slate-react';
+// @ts-ignore
+import { Value, Change, Document } from 'slate';
+// @ts-ignore
 import Plain from 'slate-plain-serializer';
 import { Popper as ReactPopper } from 'react-popper';
 import useDebounce from 'react-use/lib/useDebounce';
 import { css, cx } from 'emotion';
-
-import { SlatePrism } from '../../slate-plugins';
+// @ts-ignore
+import PluginPrism from 'slate-prism';
 
 interface DataLinkInputProps {
   value: string;
@@ -19,7 +22,7 @@ interface DataLinkInputProps {
 }
 
 const plugins = [
-  SlatePrism({
+  PluginPrism({
     onlyIn: (node: any) => node.type === 'code_block',
     getSyntax: () => 'links',
   }),
@@ -76,28 +79,27 @@ export const DataLinkInput: React.FC<DataLinkInputProps> = ({ value, onChange, s
 
   useDebounce(updateUsedSuggestions, 250, [linkUrl]);
 
-  const onKeyDown = (event: Event, editor: CoreEditor, next: Function) => {
-    const keyboardEvent = event as KeyboardEvent;
-    if (keyboardEvent.key === 'Backspace') {
+  const onKeyDown = (event: KeyboardEvent) => {
+    if (event.key === 'Backspace' || event.key === 'Escape') {
       setShowingSuggestions(false);
       setSuggestionsIndex(0);
     }
 
-    if (keyboardEvent.key === 'Enter') {
+    if (event.key === 'Enter') {
       if (showingSuggestions) {
         onVariableSelect(currentSuggestions[suggestionsIndex]);
       }
     }
 
     if (showingSuggestions) {
-      if (keyboardEvent.key === 'ArrowDown') {
-        keyboardEvent.preventDefault();
+      if (event.key === 'ArrowDown') {
+        event.preventDefault();
         setSuggestionsIndex(index => {
           return (index + 1) % currentSuggestions.length;
         });
       }
-      if (keyboardEvent.key === 'ArrowUp') {
-        keyboardEvent.preventDefault();
+      if (event.key === 'ArrowUp') {
+        event.preventDefault();
         setSuggestionsIndex(index => {
           const nextIndex = index - 1 < 0 ? currentSuggestions.length - 1 : (index - 1) % currentSuggestions.length;
           return nextIndex;
@@ -105,24 +107,21 @@ export const DataLinkInput: React.FC<DataLinkInputProps> = ({ value, onChange, s
       }
     }
 
-    if (
-      keyboardEvent.key === '?' ||
-      keyboardEvent.key === '&' ||
-      keyboardEvent.key === '$' ||
-      (keyboardEvent.keyCode === 32 && keyboardEvent.ctrlKey)
-    ) {
+    if (event.key === '?' || event.key === '&' || event.key === '$' || (event.keyCode === 32 && event.ctrlKey)) {
       setShowingSuggestions(true);
     }
 
-    if (keyboardEvent.key === 'Backspace') {
-      return next();
+    if (event.key === 'Enter' && showingSuggestions) {
+      // Preventing entering a new line
+      // As of https://github.com/ianstormtaylor/slate/issues/1345#issuecomment-340508289
+      return false;
     } else {
       // @ts-ignore
       return;
     }
   };
 
-  const onUrlChange = ({ value }: { value: Value }) => {
+  const onUrlChange = ({ value }: Change) => {
     setLinkUrl(value);
   };
 
@@ -187,7 +186,6 @@ export const DataLinkInput: React.FC<DataLinkInputProps> = ({ value, onChange, s
           </Portal>
         )}
         <Editor
-          schema={SCHEMA}
           placeholder="http://your-grafana.com/d/000000010/annotations"
           value={linkUrl}
           onChange={onUrlChange}

+ 0 - 1
packages/grafana-ui/src/index.ts

@@ -2,4 +2,3 @@ export * from './components';
 export * from './types';
 export * from './utils';
 export * from './themes';
-export * from './slate-plugins';

+ 0 - 1
packages/grafana-ui/src/slate-plugins/index.ts

@@ -1 +0,0 @@
-export { SlatePrism } from './slate-prism';

+ 0 - 3
packages/grafana-ui/src/slate-plugins/slate-prism/TOKEN_MARK.ts

@@ -1,3 +0,0 @@
-const TOKEN_MARK = 'prism-token';
-
-export default TOKEN_MARK;

+ 0 - 160
packages/grafana-ui/src/slate-plugins/slate-prism/index.ts

@@ -1,160 +0,0 @@
-import Prism from 'prismjs';
-import { Block, Text, Decoration } from 'slate';
-import { Plugin } from '@grafana/slate-react';
-import Options, { OptionsFormat } from './options';
-import TOKEN_MARK from './TOKEN_MARK';
-
-/**
- * A Slate plugin to highlight code syntax.
- */
-export function SlatePrism(optsParam: OptionsFormat = {}): Plugin {
-  const opts: Options = new Options(optsParam);
-
-  return {
-    decorateNode: (node, editor, next) => {
-      if (!opts.onlyIn(node)) {
-        return next();
-      }
-      return decorateNode(opts, Block.create(node as Block));
-    },
-
-    renderDecoration: (props, editor, next) =>
-      opts.renderDecoration(
-        {
-          children: props.children,
-          decoration: props.decoration,
-        },
-        editor as any,
-        next
-      ),
-  };
-}
-
-/**
- * Returns the decoration for a node
- */
-function decorateNode(opts: Options, block: Block) {
-  const grammarName = opts.getSyntax(block);
-  const grammar = Prism.languages[grammarName];
-  if (!grammar) {
-    // Grammar not loaded
-    return [];
-  }
-
-  // Tokenize the whole block text
-  const texts = block.getTexts();
-  const blockText = texts.map(text => text && text.getText()).join('\n');
-  const tokens = Prism.tokenize(blockText, grammar);
-
-  // The list of decorations to return
-  const decorations: Decoration[] = [];
-  let textStart = 0;
-  let textEnd = 0;
-
-  texts.forEach(text => {
-    textEnd = textStart + text!.getText().length;
-
-    let offset = 0;
-    function processToken(token: string | Prism.Token, accu?: string | number) {
-      if (typeof token === 'string') {
-        if (accu) {
-          const decoration = createDecoration({
-            text: text!,
-            textStart,
-            textEnd,
-            start: offset,
-            end: offset + token.length,
-            className: `prism-token token ${accu}`,
-            block,
-          });
-          if (decoration) {
-            decorations.push(decoration);
-          }
-        }
-        offset += token.length;
-      } else {
-        accu = `${accu} ${token.type} ${token.alias || ''}`;
-
-        if (typeof token.content === 'string') {
-          const decoration = createDecoration({
-            text: text!,
-            textStart,
-            textEnd,
-            start: offset,
-            end: offset + token.content.length,
-            className: `prism-token token ${accu}`,
-            block,
-          });
-          if (decoration) {
-            decorations.push(decoration);
-          }
-
-          offset += token.content.length;
-        } else {
-          // When using token.content instead of token.matchedStr, token can be deep
-          for (let i = 0; i < token.content.length; i += 1) {
-            // @ts-ignore
-            processToken(token.content[i], accu);
-          }
-        }
-      }
-    }
-
-    tokens.forEach(processToken);
-    textStart = textEnd + 1; // account for added `\n`
-  });
-
-  return decorations;
-}
-
-/**
- * Return a decoration range for the given text.
- */
-function createDecoration({
-  text,
-  textStart,
-  textEnd,
-  start,
-  end,
-  className,
-  block,
-}: {
-  text: Text; // The text being decorated
-  textStart: number; // Its start position in the whole text
-  textEnd: number; // Its end position in the whole text
-  start: number; // The position in the whole text where the token starts
-  end: number; // The position in the whole text where the token ends
-  className: string; // The prism token classname
-  block: Block;
-}): Decoration | null {
-  if (start >= textEnd || end <= textStart) {
-    // Ignore, the token is not in the text
-    return null;
-  }
-
-  // Shrink to this text boundaries
-  start = Math.max(start, textStart);
-  end = Math.min(end, textEnd);
-
-  // Now shift offsets to be relative to this text
-  start -= textStart;
-  end -= textStart;
-
-  const myDec = block.createDecoration({
-    object: 'decoration',
-    anchor: {
-      key: text.key,
-      offset: start,
-      object: 'point',
-    },
-    focus: {
-      key: text.key,
-      offset: end,
-      object: 'point',
-    },
-    type: TOKEN_MARK,
-    data: { className },
-  });
-
-  return myDec;
-}

+ 0 - 77
packages/grafana-ui/src/slate-plugins/slate-prism/options.tsx

@@ -1,77 +0,0 @@
-import React from 'react';
-import { Mark, Node, Decoration } from 'slate';
-import { Editor } from '@grafana/slate-react';
-import { Record } from 'immutable';
-
-import TOKEN_MARK from './TOKEN_MARK';
-
-export interface OptionsFormat {
-  // Determine which node should be highlighted
-  onlyIn?: (node: Node) => boolean;
-  // Returns the syntax for a node that should be highlighted
-  getSyntax?: (node: Node) => string;
-  // Render a highlighting mark in a highlighted node
-  renderMark?: ({ mark, children }: { mark: Mark; children: React.ReactNode }) => void | React.ReactNode;
-}
-
-/**
- * Default filter for code blocks
- */
-function defaultOnlyIn(node: Node): boolean {
-  return node.object === 'block' && node.type === 'code_block';
-}
-
-/**
- * Default getter for syntax
- */
-function defaultGetSyntax(node: Node): string {
-  return 'javascript';
-}
-
-/**
- * Default rendering for decorations
- */
-function defaultRenderDecoration(
-  props: { children: React.ReactNode; decoration: Decoration },
-  editor: Editor,
-  next: () => any
-): void | React.ReactNode {
-  const { decoration } = props;
-  if (decoration.type !== TOKEN_MARK) {
-    return next();
-  }
-
-  const className = decoration.data.get('className');
-  return <span className={className}>{props.children}</span>;
-}
-
-/**
- * The plugin options
- */
-class Options
-  extends Record({
-    onlyIn: defaultOnlyIn,
-    getSyntax: defaultGetSyntax,
-    renderDecoration: defaultRenderDecoration,
-  })
-  implements OptionsFormat {
-  readonly onlyIn!: (node: Node) => boolean;
-  readonly getSyntax!: (node: Node) => string;
-  readonly renderDecoration!: (
-    {
-      decoration,
-      children,
-    }: {
-      decoration: Decoration;
-      children: React.ReactNode;
-    },
-    editor: Editor,
-    next: () => any
-  ) => void | React.ReactNode;
-
-  constructor(props: OptionsFormat) {
-    super(props);
-  }
-}
-
-export default Options;

+ 14 - 13
packages/grafana-ui/src/utils/slate.ts

@@ -1,22 +1,22 @@
-import { Block, Document, Text, Value, SchemaProperties } from 'slate';
+// @ts-ignore
+import { Block, Document, Text, Value } from 'slate';
 
-export const SCHEMA: SchemaProperties = {
-  document: {
-    nodes: [
-      {
-        match: [{ type: 'paragraph' }, { type: 'code_block' }, { type: 'code_line' }],
-      },
-    ],
+const SCHEMA = {
+  blocks: {
+    paragraph: 'paragraph',
+    codeblock: 'code_block',
+    codeline: 'code_line',
   },
   inlines: {},
+  marks: {},
 };
 
-export const makeFragment = (text: string, syntax?: string): Document => {
+export const makeFragment = (text: string, syntax?: string) => {
   const lines = text.split('\n').map(line =>
     Block.create({
       type: 'code_line',
       nodes: [Text.create(line)],
-    })
+    } as any)
   );
 
   const block = Block.create({
@@ -25,17 +25,18 @@ export const makeFragment = (text: string, syntax?: string): Document => {
     },
     type: 'code_block',
     nodes: lines,
-  });
+  } as any);
 
   return Document.create({
     nodes: [block],
   });
 };
 
-export const makeValue = (text: string, syntax?: string): Value => {
+export const makeValue = (text: string, syntax?: string) => {
   const fragment = makeFragment(text, syntax);
 
   return Value.create({
     document: fragment,
-  });
+    SCHEMA,
+  } as any);
 };

+ 0 - 4
packages/grafana-ui/tsconfig.json

@@ -5,10 +5,6 @@
   "compilerOptions": {
     "rootDirs": [".", "stories"],
     "typeRoots": ["./node_modules/@types", "types"],
-    "baseUrl": "./node_modules/@types",
-    "paths": {
-      "@grafana/slate-react": ["slate-react"]
-    },
     "declarationDir": "dist",
     "outDir": "compiled"
   }

+ 41 - 0
public/app/features/explore/QueryField.test.tsx

@@ -17,4 +17,45 @@ describe('<QueryField />', () => {
     const wrapper = shallow(<QueryField initialQuery="my query" />);
     expect(wrapper.find('div').exists()).toBeTruthy();
   });
+
+  it('should execute query when enter is pressed and there are no suggestions visible', () => {
+    const wrapper = shallow(<QueryField initialQuery="my query" />);
+    const instance = wrapper.instance() as QueryField;
+    instance.executeOnChangeAndRunQueries = jest.fn();
+    const handleEnterAndTabKeySpy = jest.spyOn(instance, 'handleEnterKey');
+    instance.onKeyDown({ key: 'Enter', preventDefault: () => {} } as KeyboardEvent, {});
+    expect(handleEnterAndTabKeySpy).toBeCalled();
+    expect(instance.executeOnChangeAndRunQueries).toBeCalled();
+  });
+
+  it('should copy selected text', () => {
+    const wrapper = shallow(<QueryField initialQuery="" />);
+    const instance = wrapper.instance() as QueryField;
+    const textBlocks = ['ignore this text. copy this text'];
+    const copiedText = instance.getCopiedText(textBlocks, 18, 32);
+
+    expect(copiedText).toBe('copy this text');
+  });
+
+  it('should copy selected text across 2 lines', () => {
+    const wrapper = shallow(<QueryField initialQuery="" />);
+    const instance = wrapper.instance() as QueryField;
+    const textBlocks = ['ignore this text. start copying here', 'lorem ipsum. stop copying here. lorem ipsum'];
+    const copiedText = instance.getCopiedText(textBlocks, 18, 30);
+
+    expect(copiedText).toBe('start copying here\nlorem ipsum. stop copying here');
+  });
+
+  it('should copy selected text across > 2 lines', () => {
+    const wrapper = shallow(<QueryField initialQuery="" />);
+    const instance = wrapper.instance() as QueryField;
+    const textBlocks = [
+      'ignore this text. start copying here',
+      'lorem ipsum doler sit amet',
+      'lorem ipsum. stop copying here. lorem ipsum',
+    ];
+    const copiedText = instance.getCopiedText(textBlocks, 18, 30);
+
+    expect(copiedText).toBe('start copying here\nlorem ipsum doler sit amet\nlorem ipsum. stop copying here');
+  });
 });

+ 557 - 50
public/app/features/explore/QueryField.tsx

@@ -1,36 +1,55 @@
 import _ from 'lodash';
 import React, { Context } from 'react';
-
-import { Value, Editor as CoreEditor } from 'slate';
-import { Editor, Plugin } from '@grafana/slate-react';
+import ReactDOM from 'react-dom';
+// @ts-ignore
+import { Change, Range, Value, Block } from 'slate';
+// @ts-ignore
+import { Editor } from 'slate-react';
+// @ts-ignore
 import Plain from 'slate-plain-serializer';
 import classnames from 'classnames';
+// @ts-ignore
+import { isKeyHotkey } from 'is-hotkey';
 
-import { CompletionItemGroup, TypeaheadOutput } from 'app/types/explore';
+import { CompletionItem, CompletionItemGroup, TypeaheadOutput } from 'app/types/explore';
 
 import ClearPlugin from './slate-plugins/clear';
 import NewlinePlugin from './slate-plugins/newline';
-import SelectionShortcutsPlugin from './slate-plugins/selection_shortcuts';
-import IndentationPlugin from './slate-plugins/indentation';
-import ClipboardPlugin from './slate-plugins/clipboard';
-import RunnerPlugin from './slate-plugins/runner';
-import SuggestionsPlugin, { SuggestionsState } from './slate-plugins/suggestions';
-
-import { Typeahead } from './Typeahead';
 
-import { makeValue, SCHEMA } from '@grafana/ui';
+import { TypeaheadWithTheme } from './Typeahead';
+import { makeFragment, makeValue } from '@grafana/ui';
 
+export const TYPEAHEAD_DEBOUNCE = 100;
 export const HIGHLIGHT_WAIT = 500;
+const SLATE_TAB = '  ';
+const isIndentLeftHotkey = isKeyHotkey('mod+[');
+const isIndentRightHotkey = isKeyHotkey('mod+]');
+const isSelectLeftHotkey = isKeyHotkey('shift+left');
+const isSelectRightHotkey = isKeyHotkey('shift+right');
+const isSelectUpHotkey = isKeyHotkey('shift+up');
+const isSelectDownHotkey = isKeyHotkey('shift+down');
+const isSelectLineHotkey = isKeyHotkey('mod+l');
+
+function getSuggestionByIndex(suggestions: CompletionItemGroup[], index: number): CompletionItem {
+  // Flatten suggestion groups
+  const flattenedSuggestions = suggestions.reduce((acc, g) => acc.concat(g.items), []);
+  const correctedIndex = Math.max(index, 0) % flattenedSuggestions.length;
+  return flattenedSuggestions[correctedIndex];
+}
+
+function hasSuggestions(suggestions: CompletionItemGroup[]): boolean {
+  return suggestions && suggestions.length > 0;
+}
 
 export interface QueryFieldProps {
-  additionalPlugins?: Plugin[];
+  additionalPlugins?: any[];
   cleanText?: (text: string) => string;
   disabled?: boolean;
   initialQuery: string | null;
   onRunQuery?: () => void;
   onChange?: (value: string) => void;
-  onTypeahead?: (typeahead: TypeaheadInput) => Promise<TypeaheadOutput>;
-  onWillApplySuggestion?: (suggestion: string, state: SuggestionsState) => string;
+  onTypeahead?: (typeahead: TypeaheadInput) => TypeaheadOutput;
+  onWillApplySuggestion?: (suggestion: string, state: QueryFieldState) => string;
   placeholder?: string;
   portalOrigin?: string;
   syntax?: string;
@@ -40,19 +59,20 @@ export interface QueryFieldProps {
 export interface QueryFieldState {
   suggestions: CompletionItemGroup[];
   typeaheadContext: string | null;
+  typeaheadIndex: number;
   typeaheadPrefix: string;
   typeaheadText: string;
-  value: Value;
+  value: any;
   lastExecutedValue: Value;
 }
 
 export interface TypeaheadInput {
+  editorNode: Element;
   prefix: string;
   selection?: Selection;
   text: string;
   value: Value;
-  wrapperClasses: string[];
-  labelKey?: string;
+  wrapperNode: Element;
 }
 
 /**
@@ -63,35 +83,23 @@ export interface TypeaheadInput {
  */
 export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldState> {
   menuEl: HTMLElement | null;
-  plugins: Plugin[];
-  resetTimer: NodeJS.Timer;
+  plugins: any[];
+  resetTimer: any;
   mounted: boolean;
-  updateHighlightsTimer: Function;
-  editor: Editor;
-  typeaheadRef: Typeahead;
+  updateHighlightsTimer: any;
 
   constructor(props: QueryFieldProps, context: Context<any>) {
     super(props, context);
 
     this.updateHighlightsTimer = _.debounce(this.updateLogsHighlights, HIGHLIGHT_WAIT);
 
-    const { onTypeahead, cleanText, portalOrigin, onWillApplySuggestion } = props;
-
     // Base plugins
-    this.plugins = [
-      SuggestionsPlugin({ onTypeahead, cleanText, portalOrigin, onWillApplySuggestion, component: this }),
-      ClearPlugin(),
-      RunnerPlugin({ handler: this.executeOnChangeAndRunQueries }),
-      NewlinePlugin(),
-      SelectionShortcutsPlugin(),
-      IndentationPlugin(),
-      ClipboardPlugin(),
-      ...(props.additionalPlugins || []),
-    ].filter(p => p);
+    this.plugins = [ClearPlugin(), NewlinePlugin(), ...(props.additionalPlugins || [])].filter(p => p);
 
     this.state = {
       suggestions: [],
       typeaheadContext: null,
+      typeaheadIndex: 0,
       typeaheadPrefix: '',
       typeaheadText: '',
       value: makeValue(props.initialQuery || '', props.syntax),
@@ -101,6 +109,7 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
 
   componentDidMount() {
     this.mounted = true;
+    this.updateMenu();
   }
 
   componentWillUnmount() {
@@ -110,7 +119,7 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
 
   componentDidUpdate(prevProps: QueryFieldProps, prevState: QueryFieldState) {
     const { initialQuery, syntax } = this.props;
-    const { value } = this.state;
+    const { value, suggestions } = this.state;
 
     // if query changed from the outside
     if (initialQuery !== prevProps.initialQuery) {
@@ -119,17 +128,26 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
         this.setState({ value: makeValue(initialQuery || '', syntax) });
       }
     }
+
+    // Only update menu location when suggestion existence or text/selection changed
+    if (value !== prevState.value || hasSuggestions(suggestions) !== hasSuggestions(prevState.suggestions)) {
+      this.updateMenu();
+    }
   }
 
   UNSAFE_componentWillReceiveProps(nextProps: QueryFieldProps) {
     if (nextProps.syntaxLoaded && !this.props.syntaxLoaded) {
       // Need a bogus edit to re-render the editor after syntax has fully loaded
-      const editor = this.editor.insertText(' ').deleteBackward(1);
-      this.onChange(editor.value, true);
+      const change = this.state.value
+        .change()
+        .insertText(' ')
+        .deleteBackward();
+
+      this.onChange(change, true);
     }
   }
 
-  onChange = (value: Value, invokeParentOnValueChanged?: boolean) => {
+  onChange = ({ value }: Change, invokeParentOnValueChanged?: boolean) => {
     const documentChanged = value.document !== this.state.value.document;
     const prevValue = this.state.value;
 
@@ -145,6 +163,14 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
         }
       }
     });
+
+    // Show suggest menu on text input
+    if (documentChanged && value.selection.isCollapsed) {
+      // Need one paint to allow DOM-based typeahead rules to work
+      window.requestAnimationFrame(this.handleTypeahead);
+    } else if (!this.resetTimer) {
+      this.resetTypeahead();
+    }
   };
 
   updateLogsHighlights = () => {
@@ -168,18 +194,475 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
     }
   };
 
-  handleBlur = (event: Event, editor: CoreEditor, next: Function) => {
+  handleTypeahead = _.debounce(async () => {
+    const selection = window.getSelection();
+    const { cleanText, onTypeahead } = this.props;
+    const { value } = this.state;
+
+    if (onTypeahead && selection.anchorNode) {
+      const wrapperNode = selection.anchorNode.parentElement;
+      const editorNode = wrapperNode.closest('.slate-query-field');
+      if (!editorNode || this.state.value.isBlurred) {
+        // Not inside this editor
+        return;
+      }
+
+      const range = selection.getRangeAt(0);
+      const offset = range.startOffset;
+      const text = selection.anchorNode.textContent;
+      let prefix = text.substr(0, offset);
+
+      // Label values could have valid characters erased if `cleanText()` is
+      // blindly applied, which would undesirably interfere with suggestions
+      const labelValueMatch = prefix.match(/(?:!?=~?"?|")(.*)/);
+      if (labelValueMatch) {
+        prefix = labelValueMatch[1];
+      } else if (cleanText) {
+        prefix = cleanText(prefix);
+      }
+
+      const { suggestions, context, refresher } = onTypeahead({
+        editorNode,
+        prefix,
+        selection,
+        text,
+        value,
+        wrapperNode,
+      });
+
+      let filteredSuggestions = suggestions
+        .map(group => {
+          if (group.items) {
+            if (prefix) {
+              // Filter groups based on prefix
+              if (!group.skipFilter) {
+                group.items = group.items.filter(c => (c.filterText || c.label).length >= prefix.length);
+                if (group.prefixMatch) {
+                  group.items = group.items.filter(c => (c.filterText || c.label).indexOf(prefix) === 0);
+                } else {
+                  group.items = group.items.filter(c => (c.filterText || c.label).indexOf(prefix) > -1);
+                }
+              }
+              // Filter out the already typed value (prefix) unless it inserts custom text
+              group.items = group.items.filter(c => c.insertText || (c.filterText || c.label) !== prefix);
+            }
+
+            if (!group.skipSort) {
+              group.items = _.sortBy(group.items, (item: CompletionItem) => item.sortText || item.label);
+            }
+          }
+          return group;
+        })
+        .filter(group => group.items && group.items.length > 0); // Filter out empty groups
+
+      // Keep same object for equality checking later
+      if (_.isEqual(filteredSuggestions, this.state.suggestions)) {
+        filteredSuggestions = this.state.suggestions;
+      }
+
+      this.setState(
+        {
+          suggestions: filteredSuggestions,
+          typeaheadPrefix: prefix,
+          typeaheadContext: context,
+          typeaheadText: text,
+        },
+        () => {
+          if (refresher) {
+            refresher.then(this.handleTypeahead).catch(e => console.error(e));
+          }
+        }
+      );
+    }
+  }, TYPEAHEAD_DEBOUNCE);
+
+  applyTypeahead(change: Change, suggestion: CompletionItem): Change {
+    const { cleanText, onWillApplySuggestion, syntax } = this.props;
+    const { typeaheadPrefix, typeaheadText } = this.state;
+    let suggestionText = suggestion.insertText || suggestion.label;
+    const preserveSuffix = suggestion.kind === 'function';
+    const move = suggestion.move || 0;
+
+    if (onWillApplySuggestion) {
+      suggestionText = onWillApplySuggestion(suggestionText, { ...this.state });
+    }
+
+    this.resetTypeahead();
+
+    // Remove the current, incomplete text and replace it with the selected suggestion
+    const backward = suggestion.deleteBackwards || typeaheadPrefix.length;
+    const text = cleanText ? cleanText(typeaheadText) : typeaheadText;
+    const suffixLength = text.length - typeaheadPrefix.length;
+    const offset = typeaheadText.indexOf(typeaheadPrefix);
+    const midWord = typeaheadPrefix && ((suffixLength > 0 && offset > -1) || suggestionText === typeaheadText);
+    const forward = midWord && !preserveSuffix ? suffixLength + offset : 0;
+
+    // If new-lines, apply suggestion as block
+    if (suggestionText.match(/\n/)) {
+      const fragment = makeFragment(suggestionText, syntax);
+      return change
+        .deleteBackward(backward)
+        .deleteForward(forward)
+        .insertFragment(fragment)
+        .focus();
+    }
+
+    return change
+      .deleteBackward(backward)
+      .deleteForward(forward)
+      .insertText(suggestionText)
+      .move(move)
+      .focus();
+  }
+
+  handleEnterKey = (event: KeyboardEvent, change: Change) => {
+    event.preventDefault();
+
+    if (event.shiftKey) {
+      // pass through if shift is pressed
+      return undefined;
+    } else if (!this.menuEl) {
+      this.executeOnChangeAndRunQueries();
+      return true;
+    } else {
+      return this.selectSuggestion(change);
+    }
+  };
+
+  selectSuggestion = (change: Change) => {
+    const { typeaheadIndex, suggestions } = this.state;
+    event.preventDefault();
+
+    if (!suggestions || suggestions.length === 0) {
+      return undefined;
+    }
+
+    const suggestion = getSuggestionByIndex(suggestions, typeaheadIndex);
+    const nextChange = this.applyTypeahead(change, suggestion);
+
+    const insertTextOperation = nextChange.operations.find((operation: any) => operation.type === 'insert_text');
+    return insertTextOperation ? true : undefined;
+  };
+
+  handleTabKey = (change: Change): void => {
+    const {
+      startBlock,
+      endBlock,
+      selection: { startOffset, startKey, endOffset, endKey },
+    } = change.value;
+
+    if (this.menuEl) {
+      this.selectSuggestion(change);
+      return;
+    }
+
+    const first = startBlock.getFirstText();
+
+    const startBlockIsSelected =
+      startOffset === 0 && startKey === first.key && endOffset === first.text.length && endKey === first.key;
+
+    if (startBlockIsSelected || !startBlock.equals(endBlock)) {
+      this.handleIndent(change, 'right');
+    } else {
+      change.insertText(SLATE_TAB);
+    }
+  };
+
+  handleIndent = (change: Change, indentDirection: 'left' | 'right') => {
+    const curSelection = change.value.selection;
+    const selectedBlocks = change.value.document.getBlocksAtRange(curSelection);
+
+    if (indentDirection === 'left') {
+      for (const block of selectedBlocks) {
+        const blockWhitespace = block.text.length - block.text.trimLeft().length;
+
+        const rangeProperties = {
+          anchorKey: block.getFirstText().key,
+          anchorOffset: blockWhitespace,
+          focusKey: block.getFirstText().key,
+          focusOffset: blockWhitespace,
+        };
+
+        // @ts-ignore
+        const whitespaceToDelete = Range.create(rangeProperties);
+
+        change.deleteBackwardAtRange(whitespaceToDelete, Math.min(SLATE_TAB.length, blockWhitespace));
+      }
+    } else {
+      const { startText } = change.value;
+      const textBeforeCaret = startText.text.slice(0, curSelection.startOffset);
+      const isWhiteSpace = /^\s*$/.test(textBeforeCaret);
+
+      for (const block of selectedBlocks) {
+        change.insertTextByKey(block.getFirstText().key, 0, SLATE_TAB);
+      }
+
+      if (isWhiteSpace) {
+        change.moveStart(-SLATE_TAB.length);
+      }
+    }
+  };
+
+  handleSelectVertical = (change: Change, direction: 'up' | 'down') => {
+    const { focusBlock } = change.value;
+    const adjacentBlock =
+      direction === 'up'
+        ? change.value.document.getPreviousBlock(focusBlock.key)
+        : change.value.document.getNextBlock(focusBlock.key);
+
+    if (!adjacentBlock) {
+      return true;
+    }
+    const adjacentText = adjacentBlock.getFirstText();
+    change.moveFocusTo(adjacentText.key, Math.min(change.value.anchorOffset, adjacentText.text.length)).focus();
+    return true;
+  };
+
+  handleSelectUp = (change: Change) => this.handleSelectVertical(change, 'up');
+
+  handleSelectDown = (change: Change) => this.handleSelectVertical(change, 'down');
+
+  onKeyDown = (event: KeyboardEvent, change: Change) => {
+    const { typeaheadIndex } = this.state;
+
+    // Shortcuts
+    if (isIndentLeftHotkey(event)) {
+      event.preventDefault();
+      this.handleIndent(change, 'left');
+      return true;
+    } else if (isIndentRightHotkey(event)) {
+      event.preventDefault();
+      this.handleIndent(change, 'right');
+      return true;
+    } else if (isSelectLeftHotkey(event)) {
+      event.preventDefault();
+      if (change.value.focusOffset > 0) {
+        change.moveFocus(-1);
+      }
+      return true;
+    } else if (isSelectRightHotkey(event)) {
+      event.preventDefault();
+      if (change.value.focusOffset < change.value.startText.text.length) {
+        change.moveFocus(1);
+      }
+      return true;
+    } else if (isSelectUpHotkey(event)) {
+      event.preventDefault();
+      this.handleSelectUp(change);
+      return true;
+    } else if (isSelectDownHotkey(event)) {
+      event.preventDefault();
+      this.handleSelectDown(change);
+      return true;
+    } else if (isSelectLineHotkey(event)) {
+      event.preventDefault();
+      const { focusBlock, document } = change.value;
+
+      change.moveAnchorToStartOfBlock(focusBlock.key);
+
+      const nextBlock = document.getNextBlock(focusBlock.key);
+      if (nextBlock) {
+        change.moveFocusToStartOfNextBlock();
+      } else {
+        change.moveFocusToEndOfText();
+      }
+
+      return true;
+    }
+
+    switch (event.key) {
+      case 'Escape': {
+        if (this.menuEl) {
+          event.preventDefault();
+          event.stopPropagation();
+          this.resetTypeahead();
+          return true;
+        }
+        break;
+      }
+
+      case ' ': {
+        if (event.ctrlKey) {
+          event.preventDefault();
+          this.handleTypeahead();
+          return true;
+        }
+        break;
+      }
+
+      case 'Enter':
+        return this.handleEnterKey(event, change);
+
+      case 'Tab': {
+        event.preventDefault();
+        return this.handleTabKey(change);
+      }
+
+      case 'ArrowDown': {
+        if (this.menuEl) {
+          // Select next suggestion
+          event.preventDefault();
+          const itemsCount =
+            this.state.suggestions.length > 0
+              ? this.state.suggestions.reduce((totalCount, current) => totalCount + current.items.length, 0)
+              : 0;
+          this.setState({ typeaheadIndex: Math.min(itemsCount - 1, typeaheadIndex + 1) });
+        }
+        break;
+      }
+
+      case 'ArrowUp': {
+        if (this.menuEl) {
+          // Select previous suggestion
+          event.preventDefault();
+          this.setState({ typeaheadIndex: Math.max(0, typeaheadIndex - 1) });
+        }
+        break;
+      }
+
+      default: {
+        // console.log('default key', event.key, event.which, event.charCode, event.locale, data.key);
+        break;
+      }
+    }
+    return undefined;
+  };
+
+  resetTypeahead = () => {
+    if (this.mounted) {
+      this.setState({ suggestions: [], typeaheadIndex: 0, typeaheadPrefix: '', typeaheadContext: null });
+      this.resetTimer = null;
+    }
+  };
+
+  handleBlur = (event: FocusEvent, change: Change) => {
     const { lastExecutedValue } = this.state;
     const previousValue = lastExecutedValue ? Plain.serialize(this.state.lastExecutedValue) : null;
-    const currentValue = Plain.serialize(editor.value);
+    const currentValue = Plain.serialize(change.value);
+
+    // If we dont wait here, menu clicks wont work because the menu
+    // will be gone.
+    this.resetTimer = setTimeout(this.resetTypeahead, 100);
 
     if (previousValue !== currentValue) {
       this.executeOnChangeAndRunQueries();
     }
+  };
+
+  onClickMenu = (item: CompletionItem) => {
+    // Manually triggering change
+    const change = this.applyTypeahead(this.state.value.change(), item);
+    this.onChange(change, true);
+  };
+
+  updateMenu = () => {
+    const { suggestions } = this.state;
+    const menu = this.menuEl;
+    // Exit for unit tests
+    if (!window.getSelection) {
+      return;
+    }
+    const selection = window.getSelection();
+    const node = selection.anchorNode;
+
+    // No menu, nothing to do
+    if (!menu) {
+      return;
+    }
+
+    // No suggestions or blur, remove menu
+    if (!hasSuggestions(suggestions)) {
+      menu.removeAttribute('style');
+      return;
+    }
+
+    // Align menu overlay to editor node
+    if (node) {
+      // Read from DOM
+      const rect = node.parentElement.getBoundingClientRect();
+      const scrollX = window.scrollX;
+      const scrollY = window.scrollY;
+
+      // Write DOM
+      requestAnimationFrame(() => {
+        menu.style.opacity = '1';
+        menu.style.top = `${rect.top + scrollY + rect.height + 4}px`;
+        menu.style.left = `${rect.left + scrollX - 2}px`;
+      });
+    }
+  };
+
+  menuRef = (el: HTMLElement) => {
+    this.menuEl = el;
+  };
+
+  renderMenu = () => {
+    const { portalOrigin } = this.props;
+    const { suggestions, typeaheadIndex, typeaheadPrefix } = this.state;
+    if (!hasSuggestions(suggestions)) {
+      return null;
+    }
+
+    const selectedItem = getSuggestionByIndex(suggestions, typeaheadIndex);
+
+    // Create typeahead in DOM root so we can later position it absolutely
+    return (
+      <Portal origin={portalOrigin}>
+        <TypeaheadWithTheme
+          menuRef={this.menuRef}
+          selectedItem={selectedItem}
+          onClickItem={this.onClickMenu}
+          prefix={typeaheadPrefix}
+          groupedItems={suggestions}
+          typeaheadIndex={typeaheadIndex}
+        />
+      </Portal>
+    );
+  };
+
+  getCopiedText(textBlocks: string[], startOffset: number, endOffset: number) {
+    if (!textBlocks.length) {
+      return undefined;
+    }
+
+    const excludingLastLineLength = textBlocks.slice(0, -1).join('').length + textBlocks.length - 1;
+    return textBlocks.join('\n').slice(startOffset, excludingLastLineLength + endOffset);
+  }
+
+  handleCopy = (event: ClipboardEvent, change: Change) => {
+    event.preventDefault();
+
+    const { document, selection, startOffset, endOffset } = change.value;
+    const selectedBlocks = document.getBlocksAtRangeAsArray(selection).map((block: Block) => block.text);
+
+    const copiedText = this.getCopiedText(selectedBlocks, startOffset, endOffset);
+    if (copiedText) {
+      event.clipboardData.setData('Text', copiedText);
+    }
+
+    return true;
+  };
+
+  handlePaste = (event: ClipboardEvent, change: Change) => {
+    event.preventDefault();
+    const pastedValue = event.clipboardData.getData('Text');
+    const lines = pastedValue.split('\n');
+
+    if (lines.length) {
+      change.insertText(lines[0]);
+      for (const line of lines.slice(1)) {
+        change.splitBlock().insertText(line);
+      }
+    }
+
+    return true;
+  };
 
-    editor.blur();
+  handleCut = (event: ClipboardEvent, change: Change) => {
+    this.handleCopy(event, change);
+    change.deleteAtRange(change.value.selection);
 
-    return next();
+    return true;
   };
 
   render() {
@@ -187,20 +670,19 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
     const wrapperClassName = classnames('slate-query-field__wrapper', {
       'slate-query-field__wrapper--disabled': disabled,
     });
-
     return (
       <div className={wrapperClassName}>
         <div className="slate-query-field">
+          {this.renderMenu()}
           <Editor
-            ref={editor => (this.editor = editor)}
-            schema={SCHEMA}
             autoCorrect={false}
             readOnly={this.props.disabled}
             onBlur={this.handleBlur}
-            // onKeyDown={this.onKeyDown}
-            onChange={(change: { value: Value }) => {
-              this.onChange(change.value, false);
-            }}
+            onKeyDown={this.onKeyDown}
+            onChange={this.onChange}
+            onCopy={this.handleCopy}
+            onPaste={this.handlePaste}
+            onCut={this.handleCut}
             placeholder={this.props.placeholder}
             plugins={this.plugins}
             spellCheck={false}
@@ -212,4 +694,29 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
   }
 }
 
+interface PortalProps {
+  index?: number;
+  origin: string;
+}
+
+class Portal extends React.PureComponent<PortalProps, {}> {
+  node: HTMLElement;
+
+  constructor(props: PortalProps) {
+    super(props);
+    const { index = 0, origin = 'query' } = props;
+    this.node = document.createElement('div');
+    this.node.classList.add(`slate-typeahead`, `slate-typeahead-${origin}-${index}`);
+    document.body.appendChild(this.node);
+  }
+
+  componentWillUnmount() {
+    document.body.removeChild(this.node);
+  }
+
+  render() {
+    return ReactDOM.createPortal(this.props.children, this.node);
+  }
+}
+
 export default QueryField;

+ 72 - 152
public/app/features/explore/Typeahead.tsx

@@ -1,24 +1,21 @@
-import React, { createRef, CSSProperties } from 'react';
-import ReactDOM from 'react-dom';
+import React, { createRef } from 'react';
 import _ from 'lodash';
 import { FixedSizeList } from 'react-window';
 
 import { Themeable, withTheme } from '@grafana/ui';
 
-import { CompletionItem, CompletionItemKind, CompletionItemGroup } from 'app/types/explore';
+import { CompletionItem, CompletionItemGroup } from 'app/types/explore';
 import { TypeaheadItem } from './TypeaheadItem';
 import { TypeaheadInfo } from './TypeaheadInfo';
 import { flattenGroupItems, calculateLongestLabel, calculateListSizes } from './utils/typeahead';
 
-const modulo = (a: number, n: number) => a - n * Math.floor(a / n);
-
 interface Props extends Themeable {
-  origin: string;
   groupedItems: CompletionItemGroup[];
+  menuRef: any;
+  selectedItem: CompletionItem | null;
+  onClickItem: (suggestion: CompletionItem) => void;
   prefix?: string;
-  menuRef?: (el: Typeahead) => void;
-  onSelectSuggestion?: (suggestion: CompletionItem) => void;
-  isOpen?: boolean;
+  typeaheadIndex: number;
 }
 
 interface State {
@@ -26,12 +23,11 @@ interface State {
   listWidth: number;
   listHeight: number;
   itemHeight: number;
-  hoveredItem: number;
-  typeaheadIndex: number;
 }
 
 export class Typeahead extends React.PureComponent<Props, State> {
-  listRef = createRef<FixedSizeList>();
+  listRef: any = createRef();
+  documentationRef: any = createRef();
 
   constructor(props: Props) {
     super(props);
@@ -39,173 +35,97 @@ export class Typeahead extends React.PureComponent<Props, State> {
     const allItems = flattenGroupItems(props.groupedItems);
     const longestLabel = calculateLongestLabel(allItems);
     const { listWidth, listHeight, itemHeight } = calculateListSizes(props.theme, allItems, longestLabel);
-    this.state = { listWidth, listHeight, itemHeight, hoveredItem: null, typeaheadIndex: 1, allItems };
+    this.state = { listWidth, listHeight, itemHeight, allItems };
   }
 
-  componentDidMount = () => {
-    this.props.menuRef(this);
-  };
-
-  componentDidUpdate = (prevProps: Readonly<Props>, prevState: Readonly<State>) => {
-    if (prevState.typeaheadIndex !== this.state.typeaheadIndex && this.listRef && this.listRef.current) {
-      if (this.state.typeaheadIndex === 1) {
+  componentDidUpdate = (prevProps: Readonly<Props>) => {
+    if (prevProps.typeaheadIndex !== this.props.typeaheadIndex && this.listRef && this.listRef.current) {
+      if (prevProps.typeaheadIndex === 1 && this.props.typeaheadIndex === 0) {
         this.listRef.current.scrollToItem(0); // special case for handling the first group label
+        this.refreshDocumentation();
         return;
       }
-      this.listRef.current.scrollToItem(this.state.typeaheadIndex);
+      const index = this.state.allItems.findIndex(item => item === this.props.selectedItem);
+      this.listRef.current.scrollToItem(index);
+      this.refreshDocumentation();
     }
 
     if (_.isEqual(prevProps.groupedItems, this.props.groupedItems) === false) {
       const allItems = flattenGroupItems(this.props.groupedItems);
       const longestLabel = calculateLongestLabel(allItems);
       const { listWidth, listHeight, itemHeight } = calculateListSizes(this.props.theme, allItems, longestLabel);
-      this.setState({ listWidth, listHeight, itemHeight, allItems });
+      this.setState({ listWidth, listHeight, itemHeight, allItems }, () => this.refreshDocumentation());
     }
   };
 
-  onMouseEnter = (index: number) => {
-    this.setState({
-      hoveredItem: index,
-    });
-  };
-
-  onMouseLeave = () => {
-    this.setState({
-      hoveredItem: null,
-    });
-  };
-
-  moveMenuIndex = (moveAmount: number) => {
-    const itemCount = this.state.allItems.length;
-    if (itemCount) {
-      // Select next suggestion
-      event.preventDefault();
-      let newTypeaheadIndex = modulo(this.state.typeaheadIndex + moveAmount, itemCount);
-
-      if (this.state.allItems[newTypeaheadIndex].kind === CompletionItemKind.GroupTitle) {
-        newTypeaheadIndex = modulo(newTypeaheadIndex + moveAmount, itemCount);
-      }
-
-      this.setState({
-        typeaheadIndex: newTypeaheadIndex,
-      });
-
+  refreshDocumentation = () => {
+    if (!this.documentationRef.current) {
       return;
     }
-  };
 
-  insertSuggestion = () => {
-    this.props.onSelectSuggestion(this.state.allItems[this.state.typeaheadIndex]);
-  };
+    const index = this.state.allItems.findIndex(item => item === this.props.selectedItem);
+    const item = this.state.allItems[index];
 
-  get menuPosition(): CSSProperties {
-    // Exit for unit tests
-    if (!window.getSelection) {
-      return {};
+    if (item) {
+      this.documentationRef.current.refresh(item);
     }
+  };
 
-    const selection = window.getSelection();
-    const node = selection.anchorNode;
-
-    // Align menu overlay to editor node
-    if (node) {
-      // Read from DOM
-      const rect = node.parentElement.getBoundingClientRect();
-      const scrollX = window.scrollX;
-      const scrollY = window.scrollY;
-
-      return {
-        top: `${rect.top + scrollY + rect.height + 4}px`,
-        left: `${rect.left + scrollX - 2}px`,
-      };
-    }
+  onMouseEnter = (item: CompletionItem) => {
+    this.documentationRef.current.refresh(item);
+  };
 
-    return {};
-  }
+  onMouseLeave = () => {
+    this.documentationRef.current.hide();
+  };
 
   render() {
-    const { prefix, theme, isOpen, origin } = this.props;
-    const { allItems, listWidth, listHeight, itemHeight, hoveredItem, typeaheadIndex } = this.state;
-
-    const showDocumentation = hoveredItem || typeaheadIndex;
+    const { menuRef, selectedItem, onClickItem, prefix, theme } = this.props;
+    const { listWidth, listHeight, itemHeight, allItems } = this.state;
 
     return (
-      <Portal origin={origin} isOpen={isOpen}>
-        <ul className="typeahead" style={this.menuPosition}>
-          <FixedSizeList
-            ref={this.listRef}
-            itemCount={allItems.length}
-            itemSize={itemHeight}
-            itemKey={index => {
-              const item = allItems && allItems[index];
-              const key = item ? `${index}-${item.label}` : `${index}`;
-              return key;
-            }}
-            width={listWidth}
-            height={listHeight}
-          >
-            {({ index, style }) => {
-              const item = allItems && allItems[index];
-              if (!item) {
-                return null;
-              }
-
-              return (
-                <TypeaheadItem
-                  onClickItem={() => this.props.onSelectSuggestion(item)}
-                  isSelected={allItems[typeaheadIndex] === item}
-                  item={item}
-                  prefix={prefix}
-                  style={style}
-                  onMouseEnter={() => this.onMouseEnter(index)}
-                  onMouseLeave={this.onMouseLeave}
-                />
-              );
-            }}
-          </FixedSizeList>
-        </ul>
-
-        {showDocumentation && (
-          <TypeaheadInfo
-            width={listWidth}
-            height={listHeight}
-            theme={theme}
-            item={allItems[hoveredItem ? hoveredItem : typeaheadIndex]}
-          />
-        )}
-      </Portal>
+      <ul className="typeahead" ref={menuRef}>
+        <TypeaheadInfo
+          ref={this.documentationRef}
+          width={listWidth}
+          height={listHeight}
+          theme={theme}
+          initialItem={selectedItem}
+        />
+        <FixedSizeList
+          ref={this.listRef}
+          itemCount={allItems.length}
+          itemSize={itemHeight}
+          itemKey={index => {
+            const item = allItems && allItems[index];
+            const key = item ? `${index}-${item.label}` : `${index}`;
+            return key;
+          }}
+          width={listWidth}
+          height={listHeight}
+        >
+          {({ index, style }) => {
+            const item = allItems && allItems[index];
+            if (!item) {
+              return null;
+            }
+
+            return (
+              <TypeaheadItem
+                onClickItem={onClickItem}
+                isSelected={selectedItem === item}
+                item={item}
+                prefix={prefix}
+                style={style}
+                onMouseEnter={this.onMouseEnter}
+                onMouseLeave={this.onMouseLeave}
+              />
+            );
+          }}
+        </FixedSizeList>
+      </ul>
     );
   }
 }
 
 export const TypeaheadWithTheme = withTheme(Typeahead);
-
-interface PortalProps {
-  index?: number;
-  isOpen: boolean;
-  origin: string;
-}
-
-class Portal extends React.PureComponent<PortalProps, {}> {
-  node: HTMLElement;
-
-  constructor(props: PortalProps) {
-    super(props);
-    const { index = 0, origin = 'query' } = props;
-    this.node = document.createElement('div');
-    this.node.classList.add(`slate-typeahead`, `slate-typeahead-${origin}-${index}`);
-    document.body.appendChild(this.node);
-  }
-
-  componentWillUnmount() {
-    document.body.removeChild(this.node);
-  }
-
-  render() {
-    if (this.props.isOpen) {
-      return ReactDOM.createPortal(this.props.children, this.node);
-    }
-
-    return null;
-  }
-}

+ 18 - 7
public/app/features/explore/TypeaheadInfo.tsx

@@ -1,26 +1,29 @@
 import React, { PureComponent } from 'react';
-import { css, cx } from 'emotion';
-
 import { Themeable, selectThemeVariant } from '@grafana/ui';
+import { css, cx } from 'emotion';
 
 import { CompletionItem } from 'app/types/explore';
 
 interface Props extends Themeable {
-  item: CompletionItem;
+  initialItem: CompletionItem;
   width: number;
   height: number;
 }
 
-export class TypeaheadInfo extends PureComponent<Props> {
+interface State {
+  item: CompletionItem;
+}
+
+export class TypeaheadInfo extends PureComponent<Props, State> {
   constructor(props: Props) {
     super(props);
+    this.state = { item: props.initialItem };
   }
 
   getStyles = (visible: boolean) => {
     const { width, height, theme } = this.props;
     const selection = window.getSelection();
     const node = selection.anchorNode;
-
     if (!node) {
       return {};
     }
@@ -35,7 +38,7 @@ export class TypeaheadInfo extends PureComponent<Props> {
     return {
       typeaheadItem: css`
         label: type-ahead-item;
-        z-index: 500;
+        z-index: auto;
         padding: ${theme.spacing.sm} ${theme.spacing.sm} ${theme.spacing.sm} ${theme.spacing.md};
         border-radius: ${theme.border.radius.md};
         border: ${selectThemeVariant(
@@ -61,8 +64,16 @@ export class TypeaheadInfo extends PureComponent<Props> {
     };
   };
 
+  refresh = (item: CompletionItem) => {
+    this.setState({ item });
+  };
+
+  hide = () => {
+    this.setState({ item: null });
+  };
+
   render() {
-    const { item } = this.props;
+    const { item } = this.state;
     const visible = item && !!item.documentation;
     const label = item ? item.label : '';
     const documentation = item && item.documentation ? item.documentation : '';

+ 17 - 19
public/app/features/explore/TypeaheadItem.tsx

@@ -1,21 +1,25 @@
 import React, { FunctionComponent, useContext } from 'react';
-
 // @ts-ignore
 import Highlighter from 'react-highlight-words';
 import { css, cx } from 'emotion';
 import { GrafanaTheme, ThemeContext, selectThemeVariant } from '@grafana/ui';
 
-import { CompletionItem, CompletionItemKind } from 'app/types/explore';
+import { CompletionItem } from 'app/types/explore';
+
+export const GROUP_TITLE_KIND = 'GroupTitle';
+
+export const isGroupTitle = (item: CompletionItem) => {
+  return item.kind && item.kind === GROUP_TITLE_KIND ? true : false;
+};
 
 interface Props {
   isSelected: boolean;
   item: CompletionItem;
-  style: any;
+  onClickItem: (suggestion: CompletionItem) => void;
   prefix?: string;
-
-  onClickItem?: (event: React.MouseEvent) => void;
-  onMouseEnter?: () => void;
-  onMouseLeave?: () => void;
+  style: any;
+  onMouseEnter: (item: CompletionItem) => void;
+  onMouseLeave: (item: CompletionItem) => void;
 }
 
 const getStyles = (theme: GrafanaTheme) => ({
@@ -34,12 +38,10 @@ const getStyles = (theme: GrafanaTheme) => ({
     transition: color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), border-color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1),
       background 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), padding 0.15s cubic-bezier(0.645, 0.045, 0.355, 1);
   `,
-
   typeaheadItemSelected: css`
     label: type-ahead-item-selected;
     background-color: ${selectThemeVariant({ light: theme.colors.gray6, dark: theme.colors.dark9 }, theme.type)};
   `,
-
   typeaheadItemMatch: css`
     label: type-ahead-item-match;
     color: ${theme.colors.yellow};
@@ -47,7 +49,6 @@ const getStyles = (theme: GrafanaTheme) => ({
     padding: inherit;
     background: inherit;
   `,
-
   typeaheadItemGroupTitle: css`
     label: type-ahead-item-group-title;
     color: ${theme.colors.textWeak};
@@ -61,13 +62,16 @@ export const TypeaheadItem: FunctionComponent<Props> = (props: Props) => {
   const theme = useContext(ThemeContext);
   const styles = getStyles(theme);
 
-  const { isSelected, item, prefix, style, onMouseEnter, onMouseLeave, onClickItem } = props;
+  const { isSelected, item, prefix, style, onClickItem } = props;
+  const onClick = () => onClickItem(item);
+  const onMouseEnter = () => props.onMouseEnter(item);
+  const onMouseLeave = () => props.onMouseLeave(item);
   const className = isSelected ? cx([styles.typeaheadItem, styles.typeaheadItemSelected]) : cx([styles.typeaheadItem]);
   const highlightClassName = cx([styles.typeaheadItemMatch]);
   const itemGroupTitleClassName = cx([styles.typeaheadItemGroupTitle]);
   const label = item.label || '';
 
-  if (item.kind === CompletionItemKind.GroupTitle) {
+  if (isGroupTitle(item)) {
     return (
       <li className={itemGroupTitleClassName} style={style}>
         <span>{label}</span>
@@ -76,13 +80,7 @@ export const TypeaheadItem: FunctionComponent<Props> = (props: Props) => {
   }
 
   return (
-    <li
-      className={className}
-      style={style}
-      onMouseDown={onClickItem}
-      onMouseEnter={onMouseEnter}
-      onMouseLeave={onMouseLeave}
-    >
+    <li className={className} onClick={onClick} style={style} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
       <Highlighter textToHighlight={label} searchWords={[prefix]} highlightClassName={highlightClassName} />
     </li>
   );

+ 39 - 0
public/app/features/explore/slate-plugins/braces.test.ts

@@ -0,0 +1,39 @@
+// @ts-ignore
+import Plain from 'slate-plain-serializer';
+
+import BracesPlugin from './braces';
+
+declare global {
+  interface Window {
+    KeyboardEvent: any;
+  }
+}
+
+describe('braces', () => {
+  const handler = BracesPlugin().onKeyDown;
+
+  it('adds closing braces around empty value', () => {
+    const change = Plain.deserialize('').change();
+    const event = new window.KeyboardEvent('keydown', { key: '(' });
+    handler(event, change);
+    expect(Plain.serialize(change.value)).toEqual('()');
+  });
+
+  it('removes closing brace when opening brace is removed', () => {
+    const change = Plain.deserialize('time()').change();
+    let event;
+    change.move(5);
+    event = new window.KeyboardEvent('keydown', { key: 'Backspace' });
+    handler(event, change);
+    expect(Plain.serialize(change.value)).toEqual('time');
+  });
+
+  it('keeps closing brace when opening brace is removed and inner values exist', () => {
+    const change = Plain.deserialize('time(value)').change();
+    let event;
+    change.move(5);
+    event = new window.KeyboardEvent('keydown', { key: 'Backspace' });
+    const handled = handler(event, change);
+    expect(handled).toBeFalsy();
+  });
+});

+ 0 - 40
public/app/features/explore/slate-plugins/braces.test.tsx

@@ -1,40 +0,0 @@
-import React from 'react';
-import Plain from 'slate-plain-serializer';
-import { Editor } from '@grafana/slate-react';
-import { shallow } from 'enzyme';
-import BracesPlugin from './braces';
-
-declare global {
-  interface Window {
-    KeyboardEvent: any;
-  }
-}
-
-describe('braces', () => {
-  const handler = BracesPlugin().onKeyDown;
-  const nextMock = () => {};
-
-  it('adds closing braces around empty value', () => {
-    const value = Plain.deserialize('');
-    const editor = shallow<Editor>(<Editor value={value} />);
-    const event = new window.KeyboardEvent('keydown', { key: '(' });
-    handler(event as Event, editor.instance() as any, nextMock);
-    expect(Plain.serialize(editor.instance().value)).toEqual('()');
-  });
-
-  it('removes closing brace when opening brace is removed', () => {
-    const value = Plain.deserialize('time()');
-    const editor = shallow<Editor>(<Editor value={value} />);
-    const event = new window.KeyboardEvent('keydown', { key: 'Backspace' });
-    handler(event as Event, editor.instance().moveForward(5) as any, nextMock);
-    expect(Plain.serialize(editor.instance().value)).toEqual('time');
-  });
-
-  it('keeps closing brace when opening brace is removed and inner values exist', () => {
-    const value = Plain.deserialize('time(value)');
-    const editor = shallow<Editor>(<Editor value={value} />);
-    const event = new window.KeyboardEvent('keydown', { key: 'Backspace' });
-    const handled = handler(event as Event, editor.instance().moveForward(5) as any, nextMock);
-    expect(handled).toBeFalsy();
-  });
-});

+ 18 - 22
public/app/features/explore/slate-plugins/braces.ts

@@ -1,5 +1,5 @@
-import { Plugin } from '@grafana/slate-react';
-import { Editor as CoreEditor } from 'slate';
+// @ts-ignore
+import { Change } from 'slate';
 
 const BRACES: any = {
   '[': ']',
@@ -7,37 +7,34 @@ const BRACES: any = {
   '(': ')',
 };
 
-export default function BracesPlugin(): Plugin {
+export default function BracesPlugin() {
   return {
-    onKeyDown(event: KeyboardEvent, editor: CoreEditor, next: Function) {
-      const { value } = editor;
+    onKeyDown(event: KeyboardEvent, change: Change) {
+      const { value } = change;
 
       switch (event.key) {
         case '(':
         case '{':
         case '[': {
           event.preventDefault();
-          const {
-            start: { offset: startOffset, key: startKey },
-            end: { offset: endOffset, key: endKey },
-            focus: { offset: focusOffset },
-          } = value.selection;
-          const text = value.focusText.text;
+
+          const { startOffset, startKey, endOffset, endKey, focusOffset } = value.selection;
+          const text: string = value.focusText.text;
 
           // If text is selected, wrap selected text in parens
-          if (value.selection.isExpanded) {
-            editor
+          if (value.isExpanded) {
+            change
               .insertTextByKey(startKey, startOffset, event.key)
               .insertTextByKey(endKey, endOffset + 1, BRACES[event.key])
-              .moveEndBackward(1);
+              .moveEnd(-1);
           } else if (
             focusOffset === text.length ||
             text[focusOffset] === ' ' ||
             Object.values(BRACES).includes(text[focusOffset])
           ) {
-            editor.insertText(`${event.key}${BRACES[event.key]}`).moveBackward(1);
+            change.insertText(`${event.key}${BRACES[event.key]}`).move(-1);
           } else {
-            editor.insertText(event.key);
+            change.insertText(event.key);
           }
 
           return true;
@@ -45,15 +42,15 @@ export default function BracesPlugin(): Plugin {
 
         case 'Backspace': {
           const text = value.anchorText.text;
-          const offset = value.selection.anchor.offset;
+          const offset = value.anchorOffset;
           const previousChar = text[offset - 1];
           const nextChar = text[offset];
           if (BRACES[previousChar] && BRACES[previousChar] === nextChar) {
             event.preventDefault();
             // Remove closing brace if directly following
-            editor
-              .deleteBackward(1)
-              .deleteForward(1)
+            change
+              .deleteBackward()
+              .deleteForward()
               .focus();
             return true;
           }
@@ -63,8 +60,7 @@ export default function BracesPlugin(): Plugin {
           break;
         }
       }
-
-      return next();
+      return undefined;
     },
   };
 }

+ 39 - 0
public/app/features/explore/slate-plugins/clear.test.ts

@@ -0,0 +1,39 @@
+// @ts-ignore
+import Plain from 'slate-plain-serializer';
+
+import ClearPlugin from './clear';
+
+describe('clear', () => {
+  const handler = ClearPlugin().onKeyDown;
+
+  it('does not change the empty value', () => {
+    const change = Plain.deserialize('').change();
+    const event = new window.KeyboardEvent('keydown', {
+      key: 'k',
+      ctrlKey: true,
+    });
+    handler(event, change);
+    expect(Plain.serialize(change.value)).toEqual('');
+  });
+
+  it('clears to the end of the line', () => {
+    const change = Plain.deserialize('foo').change();
+    const event = new window.KeyboardEvent('keydown', {
+      key: 'k',
+      ctrlKey: true,
+    });
+    handler(event, change);
+    expect(Plain.serialize(change.value)).toEqual('');
+  });
+
+  it('clears from the middle to the end of the line', () => {
+    const change = Plain.deserialize('foo bar').change();
+    change.move(4);
+    const event = new window.KeyboardEvent('keydown', {
+      key: 'k',
+      ctrlKey: true,
+    });
+    handler(event, change);
+    expect(Plain.serialize(change.value)).toEqual('foo ');
+  });
+});

+ 0 - 42
public/app/features/explore/slate-plugins/clear.test.tsx

@@ -1,42 +0,0 @@
-import Plain from 'slate-plain-serializer';
-import React from 'react';
-import { Editor } from '@grafana/slate-react';
-import { shallow } from 'enzyme';
-import ClearPlugin from './clear';
-
-describe('clear', () => {
-  const handler = ClearPlugin().onKeyDown;
-
-  it('does not change the empty value', () => {
-    const value = Plain.deserialize('');
-    const editor = shallow<Editor>(<Editor value={value} />);
-    const event = new window.KeyboardEvent('keydown', {
-      key: 'k',
-      ctrlKey: true,
-    });
-    handler(event as Event, editor.instance() as any, () => {});
-    expect(Plain.serialize(editor.instance().value)).toEqual('');
-  });
-
-  it('clears to the end of the line', () => {
-    const value = Plain.deserialize('foo');
-    const editor = shallow<Editor>(<Editor value={value} />);
-    const event = new window.KeyboardEvent('keydown', {
-      key: 'k',
-      ctrlKey: true,
-    });
-    handler(event as Event, editor.instance() as any, () => {});
-    expect(Plain.serialize(editor.instance().value)).toEqual('');
-  });
-
-  it('clears from the middle to the end of the line', () => {
-    const value = Plain.deserialize('foo bar');
-    const editor = shallow<Editor>(<Editor value={value} />);
-    const event = new window.KeyboardEvent('keydown', {
-      key: 'k',
-      ctrlKey: true,
-    });
-    handler(event as Event, editor.instance().moveForward(4) as any, () => {});
-    expect(Plain.serialize(editor.instance().value)).toEqual('foo ');
-  });
-});

+ 8 - 13
public/app/features/explore/slate-plugins/clear.ts

@@ -1,27 +1,22 @@
-import { Plugin } from '@grafana/slate-react';
-import { Editor as CoreEditor } from 'slate';
-
 // Clears the rest of the line after the caret
-export default function ClearPlugin(): Plugin {
+export default function ClearPlugin() {
   return {
-    onKeyDown(event: KeyboardEvent, editor: CoreEditor, next: Function) {
-      const value = editor.value;
-
-      if (value.selection.isExpanded) {
-        return next();
+    onKeyDown(event: any, change: { value?: any; deleteForward?: any }) {
+      const { value } = change;
+      if (!value.isCollapsed) {
+        return undefined;
       }
 
       if (event.key === 'k' && event.ctrlKey) {
         event.preventDefault();
         const text = value.anchorText.text;
-        const offset = value.selection.anchor.offset;
+        const offset = value.anchorOffset;
         const length = text.length;
         const forward = length - offset;
-        editor.deleteForward(forward);
+        change.deleteForward(forward);
         return true;
       }
-
-      return next();
+      return undefined;
     },
   };
 }

+ 0 - 61
public/app/features/explore/slate-plugins/clipboard.ts

@@ -1,61 +0,0 @@
-import { Plugin } from '@grafana/slate-react';
-import { Editor as CoreEditor } from 'slate';
-
-const getCopiedText = (textBlocks: string[], startOffset: number, endOffset: number) => {
-  if (!textBlocks.length) {
-    return undefined;
-  }
-
-  const excludingLastLineLength = textBlocks.slice(0, -1).join('').length + textBlocks.length - 1;
-  return textBlocks.join('\n').slice(startOffset, excludingLastLineLength + endOffset);
-};
-
-export default function ClipboardPlugin(): Plugin {
-  const clipboardPlugin = {
-    onCopy(event: ClipboardEvent, editor: CoreEditor) {
-      event.preventDefault();
-
-      const { document, selection } = editor.value;
-      const {
-        start: { offset: startOffset },
-        end: { offset: endOffset },
-      } = selection;
-      const selectedBlocks = document
-        .getLeafBlocksAtRange(selection)
-        .toArray()
-        .map(block => block.text);
-
-      const copiedText = getCopiedText(selectedBlocks, startOffset, endOffset);
-      if (copiedText) {
-        event.clipboardData.setData('Text', copiedText);
-      }
-
-      return true;
-    },
-
-    onPaste(event: ClipboardEvent, editor: CoreEditor) {
-      event.preventDefault();
-      const pastedValue = event.clipboardData.getData('Text');
-      const lines = pastedValue.split('\n');
-
-      if (lines.length) {
-        editor.insertText(lines[0]);
-        for (const line of lines.slice(1)) {
-          editor.splitBlock().insertText(line);
-        }
-      }
-
-      return true;
-    },
-  };
-
-  return {
-    ...clipboardPlugin,
-    onCut(event: ClipboardEvent, editor: CoreEditor) {
-      clipboardPlugin.onCopy(event, editor);
-      editor.deleteAtRange(editor.value.selection);
-
-      return true;
-    },
-  };
-}

+ 0 - 93
public/app/features/explore/slate-plugins/indentation.ts

@@ -1,93 +0,0 @@
-import { RangeJSON, Range as SlateRange, Editor as CoreEditor } from 'slate';
-import { Plugin } from '@grafana/slate-react';
-import { isKeyHotkey } from 'is-hotkey';
-
-const isIndentLeftHotkey = isKeyHotkey('mod+[');
-const isShiftTabHotkey = isKeyHotkey('shift+tab');
-const isIndentRightHotkey = isKeyHotkey('mod+]');
-
-const SLATE_TAB = '  ';
-
-const handleTabKey = (event: KeyboardEvent, editor: CoreEditor, next: Function): void => {
-  const {
-    startBlock,
-    endBlock,
-    selection: {
-      start: { offset: startOffset, key: startKey },
-      end: { offset: endOffset, key: endKey },
-    },
-  } = editor.value;
-
-  const first = startBlock.getFirstText();
-
-  const startBlockIsSelected =
-    startOffset === 0 && startKey === first.key && endOffset === first.text.length && endKey === first.key;
-
-  if (startBlockIsSelected || !startBlock.equals(endBlock)) {
-    handleIndent(editor, 'right');
-  } else {
-    editor.insertText(SLATE_TAB);
-  }
-};
-
-const handleIndent = (editor: CoreEditor, indentDirection: 'left' | 'right') => {
-  const curSelection = editor.value.selection;
-  const selectedBlocks = editor.value.document.getLeafBlocksAtRange(curSelection).toArray();
-
-  if (indentDirection === 'left') {
-    for (const block of selectedBlocks) {
-      const blockWhitespace = block.text.length - block.text.trimLeft().length;
-
-      const textKey = block.getFirstText().key;
-
-      const rangeProperties: RangeJSON = {
-        anchor: {
-          key: textKey,
-          offset: blockWhitespace,
-          path: [],
-        },
-        focus: {
-          key: textKey,
-          offset: blockWhitespace,
-          path: [],
-        },
-      };
-
-      editor.deleteBackwardAtRange(SlateRange.create(rangeProperties), Math.min(SLATE_TAB.length, blockWhitespace));
-    }
-  } else {
-    const { startText } = editor.value;
-    const textBeforeCaret = startText.text.slice(0, curSelection.start.offset);
-    const isWhiteSpace = /^\s*$/.test(textBeforeCaret);
-
-    for (const block of selectedBlocks) {
-      editor.insertTextByKey(block.getFirstText().key, 0, SLATE_TAB);
-    }
-
-    if (isWhiteSpace) {
-      editor.moveStartBackward(SLATE_TAB.length);
-    }
-  }
-};
-
-// Clears the rest of the line after the caret
-export default function IndentationPlugin(): Plugin {
-  return {
-    onKeyDown(event: KeyboardEvent, editor: CoreEditor, next: Function) {
-      if (isIndentLeftHotkey(event) || isShiftTabHotkey(event)) {
-        event.preventDefault();
-        handleIndent(editor, 'left');
-      } else if (isIndentRightHotkey(event)) {
-        event.preventDefault();
-        handleIndent(editor, 'right');
-      } else if (event.key === 'Tab') {
-        event.preventDefault();
-        handleTabKey(event, editor, next);
-      } else {
-        return next();
-      }
-
-      return true;
-    },
-  };
-}

+ 9 - 12
public/app/features/explore/slate-plugins/newline.ts

@@ -1,7 +1,7 @@
-import { Plugin } from '@grafana/slate-react';
-import { Editor as CoreEditor } from 'slate';
+// @ts-ignore
+import { Change } from 'slate';
 
-function getIndent(text: string) {
+function getIndent(text: any) {
   let offset = text.length - text.trimLeft().length;
   if (offset) {
     let indent = text[0];
@@ -13,13 +13,12 @@ function getIndent(text: string) {
   return '';
 }
 
-export default function NewlinePlugin(): Plugin {
+export default function NewlinePlugin() {
   return {
-    onKeyDown(event: KeyboardEvent, editor: CoreEditor, next: Function) {
-      const value = editor.value;
-
-      if (value.selection.isExpanded) {
-        return next();
+    onKeyDown(event: KeyboardEvent, change: Change) {
+      const { value } = change;
+      if (!value.isCollapsed) {
+        return undefined;
       }
 
       if (event.key === 'Enter' && event.shiftKey) {
@@ -29,13 +28,11 @@ export default function NewlinePlugin(): Plugin {
         const currentLineText = startBlock.text;
         const indent = getIndent(currentLineText);
 
-        return editor
+        return change
           .splitBlock()
           .insertText(indent)
           .focus();
       }
-
-      return next();
     },
   };
 }

+ 0 - 17
public/app/features/explore/slate-plugins/runner.test.tsx

@@ -1,17 +0,0 @@
-import Plain from 'slate-plain-serializer';
-import React from 'react';
-import { Editor } from '@grafana/slate-react';
-import { shallow } from 'enzyme';
-import RunnerPlugin from './runner';
-
-describe('runner', () => {
-  const mockHandler = jest.fn();
-  const handler = RunnerPlugin({ handler: mockHandler }).onKeyDown;
-
-  it('should execute query when enter is pressed and there are no suggestions visible', () => {
-    const value = Plain.deserialize('');
-    const editor = shallow<Editor>(<Editor value={value} />);
-    handler({ key: 'Enter', preventDefault: () => {} } as KeyboardEvent, editor.instance() as any, () => {});
-    expect(mockHandler).toBeCalled();
-  });
-});

+ 2 - 5
public/app/features/explore/slate-plugins/runner.ts

@@ -1,8 +1,6 @@
-import { Editor as SlateEditor } from 'slate';
-
 export default function RunnerPlugin({ handler }: any) {
   return {
-    onKeyDown(event: KeyboardEvent, editor: SlateEditor, next: Function) {
+    onKeyDown(event: any) {
       // Handle enter
       if (handler && event.key === 'Enter' && !event.shiftKey) {
         // Submit on Enter
@@ -10,8 +8,7 @@ export default function RunnerPlugin({ handler }: any) {
         handler(event);
         return true;
       }
-
-      return next();
+      return undefined;
     },
   };
 }

+ 0 - 72
public/app/features/explore/slate-plugins/selection_shortcuts.ts

@@ -1,72 +0,0 @@
-import { Plugin } from '@grafana/slate-react';
-import { Editor as CoreEditor } from 'slate';
-
-import { isKeyHotkey } from 'is-hotkey';
-
-const isSelectLeftHotkey = isKeyHotkey('shift+left');
-const isSelectRightHotkey = isKeyHotkey('shift+right');
-const isSelectUpHotkey = isKeyHotkey('shift+up');
-const isSelectDownHotkey = isKeyHotkey('shift+down');
-const isSelectLineHotkey = isKeyHotkey('mod+l');
-
-const handleSelectVertical = (editor: CoreEditor, direction: 'up' | 'down') => {
-  const { focusBlock } = editor.value;
-  const adjacentBlock =
-    direction === 'up'
-      ? editor.value.document.getPreviousBlock(focusBlock.key)
-      : editor.value.document.getNextBlock(focusBlock.key);
-
-  if (!adjacentBlock) {
-    return true;
-  }
-  const adjacentText = adjacentBlock.getFirstText();
-  editor
-    .moveFocusTo(adjacentText.key, Math.min(editor.value.selection.anchor.offset, adjacentText.text.length))
-    .focus();
-  return true;
-};
-
-const handleSelectUp = (editor: CoreEditor) => handleSelectVertical(editor, 'up');
-
-const handleSelectDown = (editor: CoreEditor) => handleSelectVertical(editor, 'down');
-
-// Clears the rest of the line after the caret
-export default function SelectionShortcutsPlugin(): Plugin {
-  return {
-    onKeyDown(event: KeyboardEvent, editor: CoreEditor, next: Function) {
-      if (isSelectLeftHotkey(event)) {
-        event.preventDefault();
-        if (editor.value.selection.focus.offset > 0) {
-          editor.moveFocusBackward(1);
-        }
-      } else if (isSelectRightHotkey(event)) {
-        event.preventDefault();
-        if (editor.value.selection.focus.offset < editor.value.startText.text.length) {
-          editor.moveFocusForward(1);
-        }
-      } else if (isSelectUpHotkey(event)) {
-        event.preventDefault();
-        handleSelectUp(editor);
-      } else if (isSelectDownHotkey(event)) {
-        event.preventDefault();
-        handleSelectDown(editor);
-      } else if (isSelectLineHotkey(event)) {
-        event.preventDefault();
-        const { focusBlock, document } = editor.value;
-
-        editor.moveAnchorToStartOfBlock();
-
-        const nextBlock = document.getNextBlock(focusBlock.key);
-        if (nextBlock) {
-          editor.moveFocusToStartOfNextBlock();
-        } else {
-          editor.moveFocusToEndOfText();
-        }
-      } else {
-        return next();
-      }
-
-      return true;
-    },
-  };
-}

+ 0 - 313
public/app/features/explore/slate-plugins/suggestions.tsx

@@ -1,313 +0,0 @@
-import React from 'react';
-import debounce from 'lodash/debounce';
-import sortBy from 'lodash/sortBy';
-
-import { Editor as CoreEditor } from 'slate';
-import { Plugin as SlatePlugin } from '@grafana/slate-react';
-import { TypeaheadOutput, CompletionItem, CompletionItemGroup } from 'app/types';
-
-import { QueryField, TypeaheadInput } from '../QueryField';
-import TOKEN_MARK from '@grafana/ui/src/slate-plugins/slate-prism/TOKEN_MARK';
-import { TypeaheadWithTheme, Typeahead } from '../Typeahead';
-
-import { makeFragment } from '@grafana/ui';
-
-export const TYPEAHEAD_DEBOUNCE = 100;
-
-export interface SuggestionsState {
-  groupedItems: CompletionItemGroup[];
-  typeaheadPrefix: string;
-  typeaheadContext: string;
-  typeaheadText: string;
-}
-
-let state: SuggestionsState = {
-  groupedItems: [],
-  typeaheadPrefix: '',
-  typeaheadContext: '',
-  typeaheadText: '',
-};
-
-export default function SuggestionsPlugin({
-  onTypeahead,
-  cleanText,
-  onWillApplySuggestion,
-  syntax,
-  portalOrigin,
-  component,
-}: {
-  onTypeahead: (typeahead: TypeaheadInput) => Promise<TypeaheadOutput>;
-  cleanText?: (text: string) => string;
-  onWillApplySuggestion?: (suggestion: string, state: SuggestionsState) => string;
-  syntax?: string;
-  portalOrigin: string;
-  component: QueryField; // Need to attach typeaheadRef here
-}): SlatePlugin {
-  return {
-    onBlur: (event, editor, next) => {
-      state = {
-        ...state,
-        groupedItems: [],
-      };
-
-      return next();
-    },
-
-    onClick: (event, editor, next) => {
-      state = {
-        ...state,
-        groupedItems: [],
-      };
-
-      return next();
-    },
-
-    onKeyDown: (event: KeyboardEvent, editor, next) => {
-      const currentSuggestions = state.groupedItems;
-
-      const hasSuggestions = currentSuggestions.length;
-
-      switch (event.key) {
-        case 'Escape': {
-          if (hasSuggestions) {
-            event.preventDefault();
-
-            state = {
-              ...state,
-              groupedItems: [],
-            };
-
-            // Bogus edit to re-render editor
-            return editor.insertText('');
-          }
-
-          break;
-        }
-
-        case 'ArrowDown':
-        case 'ArrowUp':
-          if (hasSuggestions) {
-            event.preventDefault();
-            component.typeaheadRef.moveMenuIndex(event.key === 'ArrowDown' ? 1 : -1);
-            return;
-          }
-
-          break;
-
-        case 'Enter':
-        case 'Tab': {
-          if (hasSuggestions) {
-            event.preventDefault();
-
-            component.typeaheadRef.insertSuggestion();
-            return handleTypeahead(event, editor, next, onTypeahead, cleanText);
-          }
-
-          break;
-        }
-
-        default: {
-          handleTypeahead(event, editor, next, onTypeahead, cleanText);
-          break;
-        }
-      }
-
-      return next();
-    },
-
-    commands: {
-      selectSuggestion: (editor: CoreEditor, suggestion: CompletionItem): CoreEditor => {
-        const suggestions = state.groupedItems;
-        if (!suggestions || !suggestions.length) {
-          return editor;
-        }
-
-        // @ts-ignore
-        return editor.applyTypeahead(suggestion);
-      },
-
-      applyTypeahead: (editor: CoreEditor, suggestion: CompletionItem): CoreEditor => {
-        let suggestionText = suggestion.insertText || suggestion.label;
-
-        const preserveSuffix = suggestion.kind === 'function';
-        const move = suggestion.move || 0;
-
-        const { typeaheadPrefix, typeaheadText, typeaheadContext } = state;
-
-        if (onWillApplySuggestion) {
-          suggestionText = onWillApplySuggestion(suggestionText, {
-            groupedItems: state.groupedItems,
-            typeaheadContext,
-            typeaheadPrefix,
-            typeaheadText,
-          });
-        }
-
-        // Remove the current, incomplete text and replace it with the selected suggestion
-        const backward = suggestion.deleteBackwards || typeaheadPrefix.length;
-        const text = cleanText ? cleanText(typeaheadText) : typeaheadText;
-        const suffixLength = text.length - typeaheadPrefix.length;
-        const offset = typeaheadText.indexOf(typeaheadPrefix);
-        const midWord = typeaheadPrefix && ((suffixLength > 0 && offset > -1) || suggestionText === typeaheadText);
-        const forward = midWord && !preserveSuffix ? suffixLength + offset : 0;
-
-        // If new-lines, apply suggestion as block
-        if (suggestionText.match(/\n/)) {
-          const fragment = makeFragment(suggestionText);
-          return editor
-            .deleteBackward(backward)
-            .deleteForward(forward)
-            .insertFragment(fragment)
-            .focus();
-        }
-
-        state = {
-          ...state,
-          groupedItems: [],
-        };
-
-        return editor
-          .deleteBackward(backward)
-          .deleteForward(forward)
-          .insertText(suggestionText)
-          .moveForward(move)
-          .focus();
-      },
-    },
-
-    renderEditor: (props, editor, next) => {
-      if (editor.value.selection.isExpanded) {
-        return next();
-      }
-
-      const children = next();
-
-      return (
-        <>
-          {children}
-          <TypeaheadWithTheme
-            menuRef={(el: Typeahead) => (component.typeaheadRef = el)}
-            origin={portalOrigin}
-            prefix={state.typeaheadPrefix}
-            isOpen={!!state.groupedItems.length}
-            groupedItems={state.groupedItems}
-            //@ts-ignore
-            onSelectSuggestion={editor.selectSuggestion}
-          />
-        </>
-      );
-    },
-  };
-}
-
-const handleTypeahead = debounce(
-  async (
-    event: Event,
-    editor: CoreEditor,
-    next: () => {},
-    onTypeahead?: (typeahead: TypeaheadInput) => Promise<TypeaheadOutput>,
-    cleanText?: (text: string) => string
-  ) => {
-    if (!onTypeahead) {
-      return next();
-    }
-
-    const { value } = editor;
-    const { selection } = value;
-
-    // Get decorations associated with the current line
-    const parentBlock = value.document.getClosestBlock(value.focusBlock.key);
-    const myOffset = value.selection.start.offset - 1;
-    const decorations = parentBlock.getDecorations(editor as any);
-
-    const filteredDecorations = decorations
-      .filter(
-        decoration =>
-          decoration.start.offset <= myOffset && decoration.end.offset > myOffset && decoration.type === TOKEN_MARK
-      )
-      .toArray();
-
-    const labelKeyDec = decorations
-      .filter(
-        decoration =>
-          decoration.end.offset === myOffset &&
-          decoration.type === TOKEN_MARK &&
-          decoration.data.get('className').includes('label-key')
-      )
-      .first();
-
-    const labelKey = labelKeyDec && value.focusText.text.slice(labelKeyDec.start.offset, labelKeyDec.end.offset);
-
-    const wrapperClasses = filteredDecorations
-      .map(decoration => decoration.data.get('className'))
-      .join(' ')
-      .split(' ')
-      .filter(className => className.length);
-
-    let text = value.focusText.text;
-    let prefix = text.slice(0, selection.focus.offset);
-
-    if (filteredDecorations.length) {
-      text = value.focusText.text.slice(filteredDecorations[0].start.offset, filteredDecorations[0].end.offset);
-      prefix = value.focusText.text.slice(filteredDecorations[0].start.offset, selection.focus.offset);
-    }
-
-    // Label values could have valid characters erased if `cleanText()` is
-    // blindly applied, which would undesirably interfere with suggestions
-    const labelValueMatch = prefix.match(/(?:!?=~?"?|")(.*)/);
-    if (labelValueMatch) {
-      prefix = labelValueMatch[1];
-    } else if (cleanText) {
-      prefix = cleanText(prefix);
-    }
-
-    const { suggestions, context } = await onTypeahead({
-      prefix,
-      text,
-      value,
-      wrapperClasses,
-      labelKey,
-    });
-
-    const filteredSuggestions = suggestions
-      .map(group => {
-        if (!group.items) {
-          return group;
-        }
-
-        if (prefix) {
-          // Filter groups based on prefix
-          if (!group.skipFilter) {
-            group.items = group.items.filter(c => (c.filterText || c.label).length >= prefix.length);
-            if (group.prefixMatch) {
-              group.items = group.items.filter(c => (c.filterText || c.label).startsWith(prefix));
-            } else {
-              group.items = group.items.filter(c => (c.filterText || c.label).includes(prefix));
-            }
-          }
-
-          // Filter out the already typed value (prefix) unless it inserts custom text
-          group.items = group.items.filter(c => c.insertText || (c.filterText || c.label) !== prefix);
-        }
-
-        if (!group.skipSort) {
-          group.items = sortBy(group.items, (item: CompletionItem) => item.sortText || item.label);
-        }
-
-        return group;
-      })
-      .filter(group => group.items && group.items.length); // Filter out empty groups
-
-    state = {
-      ...state,
-      groupedItems: filteredSuggestions,
-      typeaheadPrefix: prefix,
-      typeaheadContext: context,
-      typeaheadText: text,
-    };
-
-    // Bogus edit to force re-render
-    return editor.insertText('');
-  },
-  TYPEAHEAD_DEBOUNCE
-);

+ 5 - 3
public/app/features/explore/utils/typeahead.ts

@@ -1,13 +1,14 @@
 import { GrafanaTheme } from '@grafana/ui';
 import { default as calculateSize } from 'calculate-size';
 
-import { CompletionItemGroup, CompletionItem, CompletionItemKind } from 'app/types';
+import { CompletionItemGroup, CompletionItem } from 'app/types';
+import { GROUP_TITLE_KIND } from '../TypeaheadItem';
 
 export const flattenGroupItems = (groupedItems: CompletionItemGroup[]): CompletionItem[] => {
   return groupedItems.reduce((all, current) => {
     const titleItem: CompletionItem = {
       label: current.label,
-      kind: CompletionItemKind.GroupTitle,
+      kind: GROUP_TITLE_KIND,
     };
     return all.concat(titleItem, current.items);
   }, []);
@@ -55,7 +56,8 @@ export const calculateListWidth = (longestLabelWidth: number, theme: GrafanaThem
 export const calculateListHeight = (itemHeight: number, allItems: CompletionItem[]) => {
   const numberOfItemsToShow = Math.min(allItems.length, 10);
   const minHeight = 100;
-  const totalHeight = numberOfItemsToShow * itemHeight;
+  const itemsInView = allItems.slice(0, numberOfItemsToShow);
+  const totalHeight = itemsInView.length * itemHeight;
   const listHeight = Math.max(totalHeight, minHeight);
 
   return listHeight;

+ 2 - 2
public/app/features/plugins/plugin_loader.ts

@@ -10,7 +10,7 @@ import jquery from 'jquery';
 import prismjs from 'prismjs';
 import slate from 'slate';
 // @ts-ignore
-import slateReact from '@grafana/slate-react';
+import slateReact from 'slate-react';
 // @ts-ignore
 import slatePlain from 'slate-plain-serializer';
 import react from 'react';
@@ -91,7 +91,7 @@ exposeToPlugin('rxjs', {
 // Experimental modules
 exposeToPlugin('prismjs', prismjs);
 exposeToPlugin('slate', slate);
-exposeToPlugin('@grafana/slate-react', slateReact);
+exposeToPlugin('slate-react', slateReact);
 exposeToPlugin('slate-plain-serializer', slatePlain);
 exposeToPlugin('react', react);
 exposeToPlugin('react-dom', reactDom);

+ 5 - 3
public/app/plugins/datasource/elasticsearch/components/ElasticsearchQueryField.tsx

@@ -1,7 +1,9 @@
 import _ from 'lodash';
 import React from 'react';
-
-import { SlatePrism } from '@grafana/ui';
+// @ts-ignore
+import PluginPrism from 'slate-prism';
+// @ts-ignore
+import Prism from 'prismjs';
 
 // dom also includes Element polyfills
 import QueryField from 'app/features/explore/QueryField';
@@ -22,7 +24,7 @@ class ElasticsearchQueryField extends React.PureComponent<Props, State> {
     super(props, context);
 
     this.plugins = [
-      SlatePrism({
+      PluginPrism({
         onlyIn: (node: any) => node.type === 'code_block',
         getSyntax: (node: any) => 'lucene',
       }),

+ 8 - 9
public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx

@@ -1,13 +1,12 @@
 import _ from 'lodash';
+// @ts-ignore
 import Plain from 'slate-plain-serializer';
 
 import QueryField from './query_field';
 import debounce from 'lodash/debounce';
 import { DOMUtil } from '@grafana/ui';
-import { Editor as SlateEditor } from 'slate';
 
 import { KEYWORDS, functionTokens, operatorTokens, grafanaMacros } from './kusto/kusto';
-import { CompletionItem } from 'app/types';
 // import '../sass/editor.base.scss';
 
 const TYPEAHEAD_DELAY = 100;
@@ -64,7 +63,7 @@ export default class KustoQueryField extends QueryField {
     this.fetchSchema();
   }
 
-  onTypeahead = (force = false) => {
+  onTypeahead = (force?: boolean) => {
     const selection = window.getSelection();
     if (selection.anchorNode) {
       const wrapperNode = selection.anchorNode.parentElement;
@@ -197,15 +196,15 @@ export default class KustoQueryField extends QueryField {
     }
   };
 
-  applyTypeahead = (editor: SlateEditor, suggestion: CompletionItem): SlateEditor => {
+  applyTypeahead(change: any, suggestion: { text: any; type: string; deleteBackwards: any }) {
     const { typeaheadPrefix, typeaheadContext, typeaheadText } = this.state;
-    let suggestionText = suggestion.label;
+    let suggestionText = suggestion.text || suggestion;
     const move = 0;
 
     // Modify suggestion based on context
 
     const nextChar = DOMUtil.getNextCharacter();
-    if (suggestion.kind === 'function') {
+    if (suggestion.type === 'function') {
       if (!nextChar || nextChar !== '(') {
         suggestionText += '(';
       }
@@ -229,13 +228,13 @@ export default class KustoQueryField extends QueryField {
     const midWord = typeaheadPrefix && ((suffixLength > 0 && offset > -1) || suggestionText === typeaheadText);
     const forward = midWord ? suffixLength + offset : 0;
 
-    return editor
+    return change
       .deleteBackward(backward)
       .deleteForward(forward)
       .insertText(suggestionText)
-      .moveForward(move)
+      .move(move)
       .focus();
-  };
+  }
 
   // private _getFieldsSuggestions(): SuggestionGroup[] {
   //   return [

+ 36 - 23
public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/query_field.tsx

@@ -7,13 +7,14 @@ import RunnerPlugin from 'app/features/explore/slate-plugins/runner';
 import Typeahead from './typeahead';
 import { getKeybindingSrv, KeybindingSrv } from 'app/core/services/keybindingSrv';
 
-import { Block, Document, Text, Value, Editor as CoreEditor } from 'slate';
-import { Editor } from '@grafana/slate-react';
+import { Block, Document, Text, Value } from 'slate';
+// @ts-ignore
+import { Editor } from 'slate-react';
+// @ts-ignore
 import Plain from 'slate-plain-serializer';
 import ReactDOM from 'react-dom';
 import React from 'react';
 import _ from 'lodash';
-import { CompletionItem } from 'app/types';
 
 function flattenSuggestions(s: any) {
   return s ? s.reduce((acc: any, g: any) => acc.concat(g.items), []) : [];
@@ -97,7 +98,7 @@ class QueryField extends React.Component<any, any> {
     this.updateMenu();
   }
 
-  onChange = ({ value }: { value: Value }) => {
+  onChange = ({ value }: any) => {
     const changed = value.document !== this.state.value.document;
     this.setState({ value }, () => {
       if (changed) {
@@ -123,15 +124,14 @@ class QueryField extends React.Component<any, any> {
     }
   };
 
-  onKeyDown = (event: Event, editor: CoreEditor, next: Function) => {
+  onKeyDown = (event: any, change: any) => {
     const { typeaheadIndex, suggestions } = this.state;
-    const keyboardEvent = event as KeyboardEvent;
 
-    switch (keyboardEvent.key) {
+    switch (event.key) {
       case 'Escape': {
         if (this.menuEl) {
-          keyboardEvent.preventDefault();
-          keyboardEvent.stopPropagation();
+          event.preventDefault();
+          event.stopPropagation();
           this.resetTypeahead();
           return true;
         }
@@ -139,8 +139,8 @@ class QueryField extends React.Component<any, any> {
       }
 
       case ' ': {
-        if (keyboardEvent.ctrlKey) {
-          keyboardEvent.preventDefault();
+        if (event.ctrlKey) {
+          event.preventDefault();
           this.onTypeahead(true);
           return true;
         }
@@ -151,12 +151,18 @@ class QueryField extends React.Component<any, any> {
       case 'Enter': {
         if (this.menuEl) {
           // Dont blur input
-          keyboardEvent.preventDefault();
+          event.preventDefault();
           if (!suggestions || suggestions.length === 0) {
-            return next();
+            return undefined;
           }
 
-          this.applyTypeahead();
+          // Get the currently selected suggestion
+          const flattenedSuggestions = flattenSuggestions(suggestions);
+          const selected = Math.abs(typeaheadIndex);
+          const selectedIndex = selected % flattenedSuggestions.length || 0;
+          const suggestion = flattenedSuggestions[selectedIndex];
+
+          this.applyTypeahead(change, suggestion);
           return true;
         }
         break;
@@ -165,7 +171,7 @@ class QueryField extends React.Component<any, any> {
       case 'ArrowDown': {
         if (this.menuEl) {
           // Select next suggestion
-          keyboardEvent.preventDefault();
+          event.preventDefault();
           this.setState({ typeaheadIndex: typeaheadIndex + 1 });
         }
         break;
@@ -174,7 +180,7 @@ class QueryField extends React.Component<any, any> {
       case 'ArrowUp': {
         if (this.menuEl) {
           // Select previous suggestion
-          keyboardEvent.preventDefault();
+          event.preventDefault();
           this.setState({ typeaheadIndex: Math.max(0, typeaheadIndex - 1) });
         }
         break;
@@ -185,16 +191,16 @@ class QueryField extends React.Component<any, any> {
         break;
       }
     }
-    return next();
+    return undefined;
   };
 
-  onTypeahead = (change = false, item?: any): boolean | void => {
-    return change;
+  onTypeahead = (change?: boolean, item?: any) => {
+    return change || this.state.value.change();
   };
 
-  applyTypeahead = (editor?: CoreEditor, suggestion?: CompletionItem): { value: Value } => {
-    return { value: new Value() };
-  };
+  applyTypeahead(change?: boolean, suggestion?: any): { value: object } {
+    return { value: {} };
+  }
 
   resetTypeahead = () => {
     this.setState({
@@ -239,8 +245,15 @@ class QueryField extends React.Component<any, any> {
       return;
     }
 
+    // Get the currently selected suggestion
+    const flattenedSuggestions = flattenSuggestions(suggestions);
+    const suggestion: any = _.find(
+      flattenedSuggestions,
+      suggestion => suggestion.display === item || suggestion.text === item
+    );
+
     // Manually triggering change
-    const change = this.applyTypeahead();
+    const change = this.applyTypeahead(this.state.value.change(), suggestion);
     this.onChange(change);
   };
 

+ 1 - 2
public/app/plugins/datasource/loki/components/LokiQueryField.tsx

@@ -1,7 +1,6 @@
 import React, { FunctionComponent } from 'react';
 import { LokiQueryFieldForm, LokiQueryFieldFormProps } from './LokiQueryFieldForm';
 import { useLokiSyntax } from './useLokiSyntax';
-import LokiLanguageProvider from '../language_provider';
 
 export const LokiQueryField: FunctionComponent<LokiQueryFieldFormProps> = ({
   datasource,
@@ -9,7 +8,7 @@ export const LokiQueryField: FunctionComponent<LokiQueryFieldFormProps> = ({
   ...otherProps
 }) => {
   const { isSyntaxReady, setActiveOption, refreshLabels, ...syntaxProps } = useLokiSyntax(
-    datasource.languageProvider as LokiLanguageProvider,
+    datasource.languageProvider,
     datasourceStatus,
     otherProps.absoluteRange
   );

+ 23 - 26
public/app/plugins/datasource/loki/components/LokiQueryFieldForm.tsx

@@ -2,24 +2,18 @@
 import React from 'react';
 // @ts-ignore
 import Cascader from 'rc-cascader';
-
-import { SlatePrism } from '@grafana/ui';
-
+// @ts-ignore
+import PluginPrism from 'slate-prism';
 // Components
-import QueryField, { TypeaheadInput } from 'app/features/explore/QueryField';
+import QueryField, { TypeaheadInput, QueryFieldState } from 'app/features/explore/QueryField';
 // Utils & Services
 // dom also includes Element polyfills
 import BracesPlugin from 'app/features/explore/slate-plugins/braces';
-import { Plugin, Node } from 'slate';
-
 // Types
 import { LokiQuery } from '../types';
-import { TypeaheadOutput } from 'app/types/explore';
+import { TypeaheadOutput, HistoryItem } from 'app/types/explore';
 import { DataSourceApi, ExploreQueryFieldProps, DataSourceStatus, DOMUtil } from '@grafana/ui';
 import { AbsoluteTimeRange } from '@grafana/data';
-import { Grammar } from 'prismjs';
-import LokiLanguageProvider, { LokiHistoryItem } from '../language_provider';
-import { SuggestionsState } from 'app/features/explore/slate-plugins/suggestions';
 
 function getChooserText(hasSyntax: boolean, hasLogLabels: boolean, datasourceStatus: DataSourceStatus) {
   if (datasourceStatus === DataSourceStatus.Disconnected) {
@@ -34,7 +28,7 @@ function getChooserText(hasSyntax: boolean, hasLogLabels: boolean, datasourceSta
   return 'Log labels';
 }
 
-function willApplySuggestion(suggestion: string, { typeaheadContext, typeaheadText }: SuggestionsState): string {
+function willApplySuggestion(suggestion: string, { typeaheadContext, typeaheadText }: QueryFieldState): string {
   // Modify suggestion based on context
   switch (typeaheadContext) {
     case 'context-labels': {
@@ -69,17 +63,17 @@ export interface CascaderOption {
 }
 
 export interface LokiQueryFieldFormProps extends ExploreQueryFieldProps<DataSourceApi<LokiQuery>, LokiQuery> {
-  history: LokiHistoryItem[];
-  syntax: Grammar;
+  history: HistoryItem[];
+  syntax: any;
   logLabelOptions: any[];
-  syntaxLoaded: boolean;
+  syntaxLoaded: any;
   absoluteRange: AbsoluteTimeRange;
   onLoadOptions: (selectedOptions: CascaderOption[]) => void;
   onLabelsRefresh?: () => void;
 }
 
 export class LokiQueryFieldForm extends React.PureComponent<LokiQueryFieldFormProps> {
-  plugins: Plugin[];
+  plugins: any[];
   modifiedSearch: string;
   modifiedQuery: string;
 
@@ -88,9 +82,9 @@ export class LokiQueryFieldForm extends React.PureComponent<LokiQueryFieldFormPr
 
     this.plugins = [
       BracesPlugin(),
-      SlatePrism({
-        onlyIn: (node: Node) => node.object === 'block' && node.type === 'code_block',
-        getSyntax: (node: Node) => 'promql',
+      PluginPrism({
+        onlyIn: (node: any) => node.type === 'code_block',
+        getSyntax: (node: any) => 'promql',
       }),
     ];
   }
@@ -121,23 +115,27 @@ export class LokiQueryFieldForm extends React.PureComponent<LokiQueryFieldFormPr
     }
   };
 
-  onTypeahead = async (typeahead: TypeaheadInput): Promise<TypeaheadOutput> => {
+  onTypeahead = (typeahead: TypeaheadInput): TypeaheadOutput => {
     const { datasource } = this.props;
-
     if (!datasource.languageProvider) {
       return { suggestions: [] };
     }
 
-    const lokiLanguageProvider = datasource.languageProvider as LokiLanguageProvider;
     const { history, absoluteRange } = this.props;
-    const { prefix, text, value, wrapperClasses, labelKey } = typeahead;
+    const { prefix, text, value, wrapperNode } = typeahead;
+
+    // Get DOM-dependent context
+    const wrapperClasses = Array.from(wrapperNode.classList);
+    const labelKeyNode = DOMUtil.getPreviousCousin(wrapperNode, '.attr-name');
+    const labelKey = labelKeyNode && labelKeyNode.textContent;
+    const nextChar = DOMUtil.getNextCharacter();
 
-    const result = await lokiLanguageProvider.provideCompletionItems(
+    const result = datasource.languageProvider.provideCompletionItems(
       { text, value, prefix, wrapperClasses, labelKey },
       { history, absoluteRange }
     );
 
-    //console.log('handleTypeahead', wrapperClasses, text, prefix, nextChar, labelKey, result.context);
+    console.log('handleTypeahead', wrapperClasses, text, prefix, nextChar, labelKey, result.context);
 
     return result;
   };
@@ -153,8 +151,7 @@ export class LokiQueryFieldForm extends React.PureComponent<LokiQueryFieldFormPr
       datasource,
       datasourceStatus,
     } = this.props;
-    const lokiLanguageProvider = datasource.languageProvider as LokiLanguageProvider;
-    const cleanText = datasource.languageProvider ? lokiLanguageProvider.cleanText : undefined;
+    const cleanText = datasource.languageProvider ? datasource.languageProvider.cleanText : undefined;
     const hasLogLabels = logLabelOptions && logLabelOptions.length > 0;
     const chooserText = getChooserText(syntaxLoaded, hasLogLabels, datasourceStatus);
     const buttonDisabled = !syntaxLoaded || datasourceStatus === DataSourceStatus.Disconnected;

+ 0 - 1
public/app/plugins/datasource/loki/components/useLokiSyntax.test.ts

@@ -3,7 +3,6 @@ import { DataSourceStatus } from '@grafana/ui/src/types/datasource';
 import { AbsoluteTimeRange } from '@grafana/data';
 
 import LanguageProvider from 'app/plugins/datasource/loki/language_provider';
-
 import { useLokiSyntax } from './useLokiSyntax';
 import { CascaderOption } from 'app/plugins/datasource/loki/components/LokiQueryFieldForm';
 import { makeMockLokiDatasource } from '../mocks';

+ 1 - 0
public/app/plugins/datasource/loki/components/useLokiSyntax.ts

@@ -1,4 +1,5 @@
 import { useState, useEffect } from 'react';
+// @ts-ignore
 import Prism from 'prismjs';
 import { DataSourceStatus } from '@grafana/ui/src/types/datasource';
 import { AbsoluteTimeRange } from '@grafana/data';

+ 25 - 32
public/app/plugins/datasource/loki/language_provider.test.ts

@@ -1,14 +1,13 @@
+// @ts-ignore
 import Plain from 'slate-plain-serializer';
-import { Editor as SlateEditor } from 'slate';
 
 import LanguageProvider, { LABEL_REFRESH_INTERVAL, LokiHistoryItem, rangeToParams } from './language_provider';
 import { AbsoluteTimeRange } from '@grafana/data';
 import { advanceTo, clear, advanceBy } from 'jest-date-mock';
 import { beforeEach } from 'test/lib/common';
-
+import { DataSourceApi } from '@grafana/ui';
 import { TypeaheadInput } from '../../../types';
 import { makeMockLokiDatasource } from './mocks';
-import LokiDatasource from './datasource';
 
 describe('Language completion provider', () => {
   const datasource = makeMockLokiDatasource({});
@@ -19,16 +18,16 @@ describe('Language completion provider', () => {
   };
 
   describe('empty query suggestions', () => {
-    it('returns no suggestions on empty context', async () => {
+    it('returns no suggestions on empty context', () => {
       const instance = new LanguageProvider(datasource);
       const value = Plain.deserialize('');
-      const result = await instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] });
+      const result = instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] });
       expect(result.context).toBeUndefined();
-
+      expect(result.refresher).toBeUndefined();
       expect(result.suggestions.length).toEqual(0);
     });
 
-    it('returns default suggestions with history on empty context when history was provided', async () => {
+    it('returns default suggestions with history on empty context when history was provided', () => {
       const instance = new LanguageProvider(datasource);
       const value = Plain.deserialize('');
       const history: LokiHistoryItem[] = [
@@ -37,12 +36,12 @@ describe('Language completion provider', () => {
           ts: 1,
         },
       ];
-      const result = await instance.provideCompletionItems(
+      const result = instance.provideCompletionItems(
         { text: '', prefix: '', value, wrapperClasses: [] },
         { history, absoluteRange: rangeMock }
       );
       expect(result.context).toBeUndefined();
-
+      expect(result.refresher).toBeUndefined();
       expect(result.suggestions).toMatchObject([
         {
           label: 'History',
@@ -55,7 +54,7 @@ describe('Language completion provider', () => {
       ]);
     });
 
-    it('returns no suggestions within regexp', async () => {
+    it('returns no suggestions within regexp', () => {
       const instance = new LanguageProvider(datasource);
       const input = createTypeaheadInput('{} ()', '', undefined, 4, []);
       const history: LokiHistoryItem[] = [
@@ -64,28 +63,18 @@ describe('Language completion provider', () => {
           ts: 1,
         },
       ];
-      const result = await instance.provideCompletionItems(input, { history });
+      const result = instance.provideCompletionItems(input, { history });
       expect(result.context).toBeUndefined();
-
+      expect(result.refresher).toBeUndefined();
       expect(result.suggestions.length).toEqual(0);
     });
   });
 
   describe('label suggestions', () => {
-    it('returns default label suggestions on label context', async () => {
+    it('returns default label suggestions on label context', () => {
       const instance = new LanguageProvider(datasource);
-      const value = Plain.deserialize('{}');
-      const ed = new SlateEditor({ value });
-      const valueWithSelection = ed.moveForward(1).value;
-      const result = await instance.provideCompletionItems(
-        {
-          text: '',
-          prefix: '',
-          wrapperClasses: ['context-labels'],
-          value: valueWithSelection,
-        },
-        { absoluteRange: rangeMock }
-      );
+      const input = createTypeaheadInput('{}', '');
+      const result = instance.provideCompletionItems(input, { absoluteRange: rangeMock });
       expect(result.context).toBe('context-labels');
       expect(result.suggestions).toEqual([{ items: [{ label: 'job' }, { label: 'namespace' }], label: 'Labels' }]);
     });
@@ -94,7 +83,7 @@ describe('Language completion provider', () => {
       const datasource = makeMockLokiDatasource({ label1: [], label2: [] });
       const provider = await getLanguageProvider(datasource);
       const input = createTypeaheadInput('{}', '');
-      const result = await provider.provideCompletionItems(input, { absoluteRange: rangeMock });
+      const result = provider.provideCompletionItems(input, { absoluteRange: rangeMock });
       expect(result.context).toBe('context-labels');
       expect(result.suggestions).toEqual([{ items: [{ label: 'label1' }, { label: 'label2' }], label: 'Labels' }]);
     });
@@ -103,9 +92,11 @@ describe('Language completion provider', () => {
       const datasource = makeMockLokiDatasource({ label1: ['label1_val1', 'label1_val2'], label2: [] });
       const provider = await getLanguageProvider(datasource);
       const input = createTypeaheadInput('{label1=}', '=', 'label1');
-      let result = await provider.provideCompletionItems(input, { absoluteRange: rangeMock });
-
-      result = await provider.provideCompletionItems(input, { absoluteRange: rangeMock });
+      let result = provider.provideCompletionItems(input, { absoluteRange: rangeMock });
+      // The values for label are loaded adhoc and there is a promise returned that we have to wait for
+      expect(result.refresher).toBeDefined();
+      await result.refresher;
+      result = provider.provideCompletionItems(input, { absoluteRange: rangeMock });
       expect(result.context).toBe('context-label-values');
       expect(result.suggestions).toEqual([
         { items: [{ label: 'label1_val1' }, { label: 'label1_val2' }], label: 'Label values for "label1"' },
@@ -210,7 +201,7 @@ describe('Labels refresh', () => {
   });
 });
 
-async function getLanguageProvider(datasource: LokiDatasource) {
+async function getLanguageProvider(datasource: DataSourceApi) {
   const instance = new LanguageProvider(datasource);
   instance.initialRange = {
     from: Date.now() - 10000,
@@ -233,8 +224,10 @@ function createTypeaheadInput(
   wrapperClasses?: string[]
 ): TypeaheadInput {
   const deserialized = Plain.deserialize(value);
-  const range = deserialized.selection.setAnchor(deserialized.selection.anchor.setOffset(anchorOffset || 1));
-  const valueWithSelection = deserialized.setSelection(range);
+  const range = deserialized.selection.merge({
+    anchorOffset: anchorOffset || 1,
+  });
+  const valueWithSelection = deserialized.change().select(range).value;
   return {
     text,
     prefix: '',

+ 29 - 25
public/app/plugins/datasource/loki/language_provider.ts

@@ -6,12 +6,18 @@ import { parseSelector, labelRegexp, selectorRegexp } from 'app/plugins/datasour
 import syntax from './syntax';
 
 // Types
-import { CompletionItem, LanguageProvider, TypeaheadInput, TypeaheadOutput, HistoryItem } from 'app/types/explore';
+import {
+  CompletionItem,
+  CompletionItemGroup,
+  LanguageProvider,
+  TypeaheadInput,
+  TypeaheadOutput,
+  HistoryItem,
+} from 'app/types/explore';
 import { LokiQuery } from './types';
 import { dateTime, AbsoluteTimeRange } from '@grafana/data';
 import { PromQuery } from '../prometheus/types';
-
-import LokiDatasource from './datasource';
+import { DataSourceApi } from '@grafana/ui';
 
 const DEFAULT_KEYS = ['job', 'namespace'];
 const EMPTY_SELECTOR = '{}';
@@ -53,9 +59,8 @@ export default class LokiLanguageProvider extends LanguageProvider {
   logLabelFetchTs?: number;
   started: boolean;
   initialRange: AbsoluteTimeRange;
-  datasource: LokiDatasource;
 
-  constructor(datasource: LokiDatasource, initialValues?: any) {
+  constructor(datasource: DataSourceApi, initialValues?: any) {
     super();
 
     this.datasource = datasource;
@@ -64,7 +69,6 @@ export default class LokiLanguageProvider extends LanguageProvider {
 
     Object.assign(this, initialValues);
   }
-
   // Strip syntax chars
   cleanText = (s: string) => s.replace(/[{}[\]="(),!~+\-*/^%]/g, '').trim();
 
@@ -107,14 +111,14 @@ export default class LokiLanguageProvider extends LanguageProvider {
    * @param context.absoluteRange Required in case we are doing getLabelCompletionItems
    * @param context.history Optional used only in getEmptyCompletionItems
    */
-  async provideCompletionItems(input: TypeaheadInput, context?: TypeaheadContext): Promise<TypeaheadOutput> {
+  provideCompletionItems(input: TypeaheadInput, context?: TypeaheadContext): TypeaheadOutput {
     const { wrapperClasses, value } = input;
     // Local text properties
     const empty = value.document.text.length === 0;
     // Determine candidates by CSS context
     if (_.includes(wrapperClasses, 'context-labels')) {
       // Suggestions for {|} and {foo=|}
-      return await this.getLabelCompletionItems(input, context);
+      return this.getLabelCompletionItems(input, context);
     } else if (empty) {
       return this.getEmptyCompletionItems(context || {});
     }
@@ -126,7 +130,7 @@ export default class LokiLanguageProvider extends LanguageProvider {
 
   getEmptyCompletionItems(context: any): TypeaheadOutput {
     const { history } = context;
-    const suggestions = [];
+    const suggestions: CompletionItemGroup[] = [];
 
     if (history && history.length > 0) {
       const historyItems = _.chain(history)
@@ -149,14 +153,15 @@ export default class LokiLanguageProvider extends LanguageProvider {
     return { suggestions };
   }
 
-  async getLabelCompletionItems(
+  getLabelCompletionItems(
     { text, wrapperClasses, labelKey, value }: TypeaheadInput,
     { absoluteRange }: any
-  ): Promise<TypeaheadOutput> {
+  ): TypeaheadOutput {
     let context: string;
-    const suggestions = [];
+    let refresher: Promise<any> = null;
+    const suggestions: CompletionItemGroup[] = [];
     const line = value.anchorBlock.getText();
-    const cursorOffset: number = value.selection.anchor.offset;
+    const cursorOffset: number = value.anchorOffset;
 
     // Use EMPTY_SELECTOR until series API is implemented for facetting
     const selector = EMPTY_SELECTOR;
@@ -166,20 +171,19 @@ export default class LokiLanguageProvider extends LanguageProvider {
     } catch {}
     const existingKeys = parsedSelector ? parsedSelector.labelKeys : [];
 
-    if ((text && text.match(/^!?=~?/)) || wrapperClasses.includes('attr-value')) {
+    if ((text && text.match(/^!?=~?/)) || _.includes(wrapperClasses, 'attr-value')) {
       // Label values
       if (labelKey && this.labelValues[selector]) {
-        let labelValues = this.labelValues[selector][labelKey];
-        if (!labelValues) {
-          await this.fetchLabelValues(labelKey, absoluteRange);
-          labelValues = this.labelValues[selector][labelKey];
+        const labelValues = this.labelValues[selector][labelKey];
+        if (labelValues) {
+          context = 'context-label-values';
+          suggestions.push({
+            label: `Label values for "${labelKey}"`,
+            items: labelValues.map(wrapLabel),
+          });
+        } else {
+          refresher = this.fetchLabelValues(labelKey, absoluteRange);
         }
-
-        context = 'context-label-values';
-        suggestions.push({
-          label: `Label values for "${labelKey}"`,
-          items: labelValues.map(wrapLabel),
-        });
       }
     } else {
       // Label keys
@@ -193,7 +197,7 @@ export default class LokiLanguageProvider extends LanguageProvider {
       }
     }
 
-    return { context, suggestions };
+    return { context, refresher, suggestions };
   }
 
   async importQueries(queries: LokiQuery[], datasourceType: string): Promise<LokiQuery[]> {

+ 2 - 2
public/app/plugins/datasource/loki/mocks.ts

@@ -1,6 +1,6 @@
-import LokiDatasource from './datasource';
+import { DataSourceApi } from '@grafana/ui';
 
-export function makeMockLokiDatasource(labelsAndValues: { [label: string]: string[] }): LokiDatasource {
+export function makeMockLokiDatasource(labelsAndValues: { [label: string]: string[] }): DataSourceApi {
   const labels = Object.keys(labelsAndValues);
   return {
     metadataRequest: (url: string) => {

+ 1 - 3
public/app/plugins/datasource/loki/syntax.ts

@@ -1,8 +1,6 @@
-import { Grammar } from 'prismjs';
-
 /* tslint:disable max-line-length */
 
-const tokenizer: Grammar = {
+const tokenizer = {
   comment: {
     pattern: /(^|[^\n])#.*/,
     lookbehind: true,

+ 19 - 15
public/app/plugins/datasource/prometheus/components/PromQueryField.tsx

@@ -2,22 +2,20 @@ import _ from 'lodash';
 import React from 'react';
 // @ts-ignore
 import Cascader from 'rc-cascader';
-
-import { SlatePrism } from '@grafana/ui';
-
+// @ts-ignore
+import PluginPrism from 'slate-prism';
+// @ts-ignore
 import Prism from 'prismjs';
 
 import { TypeaheadOutput, HistoryItem } from 'app/types/explore';
 // dom also includes Element polyfills
 import BracesPlugin from 'app/features/explore/slate-plugins/braces';
-import QueryField, { TypeaheadInput } from 'app/features/explore/QueryField';
+import QueryField, { TypeaheadInput, QueryFieldState } from 'app/features/explore/QueryField';
 import { PromQuery, PromContext, PromOptions } from '../types';
 import { CancelablePromise, makePromiseCancelable } from 'app/core/utils/CancelablePromise';
 import { ExploreQueryFieldProps, DataSourceStatus, QueryHint, DOMUtil } from '@grafana/ui';
 import { isDataFrame, toLegacyResponseData } from '@grafana/data';
 import { PrometheusDatasource } from '../datasource';
-import PromQlLanguageProvider from '../language_provider';
-import { SuggestionsState } from 'app/features/explore/slate-plugins/suggestions';
 
 const HISTOGRAM_GROUP = '__histograms__';
 const METRIC_MARK = 'metric';
@@ -69,7 +67,7 @@ export function groupMetricsByPrefix(metrics: string[], delimiter = '_'): Cascad
   return [...options, ...metricsOptions];
 }
 
-export function willApplySuggestion(suggestion: string, { typeaheadContext, typeaheadText }: SuggestionsState): string {
+export function willApplySuggestion(suggestion: string, { typeaheadContext, typeaheadText }: QueryFieldState): string {
   // Modify suggestion based on context
   switch (typeaheadContext) {
     case 'context-labels': {
@@ -104,7 +102,7 @@ interface CascaderOption {
 }
 
 interface PromQueryFieldProps extends ExploreQueryFieldProps<PrometheusDatasource, PromQuery, PromOptions> {
-  history: Array<HistoryItem<PromQuery>>;
+  history: HistoryItem[];
 }
 
 interface PromQueryFieldState {
@@ -115,7 +113,7 @@ interface PromQueryFieldState {
 
 class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryFieldState> {
   plugins: any[];
-  languageProvider: PromQlLanguageProvider;
+  languageProvider: any;
   languageProviderInitializationPromise: CancelablePromise<any>;
 
   constructor(props: PromQueryFieldProps, context: React.Context<any>) {
@@ -127,7 +125,7 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
 
     this.plugins = [
       BracesPlugin(),
-      SlatePrism({
+      PluginPrism({
         onlyIn: (node: any) => node.type === 'code_block',
         getSyntax: (node: any) => 'promql',
       }),
@@ -254,7 +252,7 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
       return;
     }
 
-    Prism.languages[PRISM_SYNTAX] = this.languageProvider.syntax;
+    Prism.languages[PRISM_SYNTAX] = this.languageProvider.getSyntax();
     Prism.languages[PRISM_SYNTAX][METRIC_MARK] = {
       alias: 'variable',
       pattern: new RegExp(`(?:^|\\s)(${metrics.join('|')})(?:$|\\s)`),
@@ -274,20 +272,26 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
     this.setState({ metricsOptions, syntaxLoaded: true });
   };
 
-  onTypeahead = async (typeahead: TypeaheadInput): Promise<TypeaheadOutput> => {
+  onTypeahead = (typeahead: TypeaheadInput): TypeaheadOutput => {
     if (!this.languageProvider) {
       return { suggestions: [] };
     }
 
     const { history } = this.props;
-    const { prefix, text, value, wrapperClasses, labelKey } = typeahead;
+    const { prefix, text, value, wrapperNode } = typeahead;
+
+    // Get DOM-dependent context
+    const wrapperClasses = Array.from(wrapperNode.classList);
+    const labelKeyNode = DOMUtil.getPreviousCousin(wrapperNode, '.attr-name');
+    const labelKey = labelKeyNode && labelKeyNode.textContent;
+    const nextChar = DOMUtil.getNextCharacter();
 
-    const result = await this.languageProvider.provideCompletionItems(
+    const result = this.languageProvider.provideCompletionItems(
       { text, value, prefix, wrapperClasses, labelKey },
       { history }
     );
 
-    // console.log('handleTypeahead', wrapperClasses, text, prefix, labelKey, result.context);
+    console.log('handleTypeahead', wrapperClasses, text, prefix, nextChar, labelKey, result.context);
 
     return result;
   };

+ 60 - 85
public/app/plugins/datasource/prometheus/language_provider.ts

@@ -1,28 +1,23 @@
 import _ from 'lodash';
 
-import { dateTime } from '@grafana/data';
-
 import {
   CompletionItem,
   CompletionItemGroup,
   LanguageProvider,
   TypeaheadInput,
   TypeaheadOutput,
-  HistoryItem,
 } from 'app/types/explore';
 
 import { parseSelector, processLabels, processHistogramLabels } from './language_utils';
 import PromqlSyntax, { FUNCTIONS, RATE_RANGES } from './promql';
-
-import { PrometheusDatasource } from './datasource';
-import { PromQuery } from './types';
+import { dateTime } from '@grafana/data';
 
 const DEFAULT_KEYS = ['job', 'instance'];
 const EMPTY_SELECTOR = '{}';
 const HISTORY_ITEM_COUNT = 5;
 const HISTORY_COUNT_CUTOFF = 1000 * 60 * 60 * 24; // 24h
 
-const wrapLabel = (label: string): CompletionItem => ({ label });
+const wrapLabel = (label: string) => ({ label });
 
 const setFunctionKind = (suggestion: CompletionItem): CompletionItem => {
   suggestion.kind = 'function';
@@ -35,12 +30,10 @@ export function addHistoryMetadata(item: CompletionItem, history: any[]): Comple
   const count = historyForItem.length;
   const recent = historyForItem[0];
   let hint = `Queried ${count} times in the last 24h.`;
-
   if (recent) {
     const lastQueried = dateTime(recent.ts).fromNow();
     hint = `${hint} Last queried ${lastQueried}.`;
   }
-
   return {
     ...item,
     documentation: hint,
@@ -54,9 +47,8 @@ export default class PromQlLanguageProvider extends LanguageProvider {
   labelValues?: { [index: string]: { [index: string]: string[] } }; // metric -> labelKey -> [labelValue,...]
   metrics?: string[];
   startTask: Promise<any>;
-  datasource: PrometheusDatasource;
 
-  constructor(datasource: PrometheusDatasource, initialValues?: any) {
+  constructor(datasource: any, initialValues?: any) {
     super();
 
     this.datasource = datasource;
@@ -68,11 +60,10 @@ export default class PromQlLanguageProvider extends LanguageProvider {
 
     Object.assign(this, initialValues);
   }
-
   // Strip syntax chars
   cleanText = (s: string) => s.replace(/[{}[\]="(),!~+\-*/^%]/g, '').trim();
 
-  get syntax() {
+  getSyntax() {
     return PromqlSyntax;
   }
 
@@ -115,46 +106,39 @@ export default class PromQlLanguageProvider extends LanguageProvider {
     }
   };
 
-  provideCompletionItems = async (
-    { prefix, text, value, labelKey, wrapperClasses }: TypeaheadInput,
-    context: { history: Array<HistoryItem<PromQuery>> } = { history: [] }
-  ): Promise<TypeaheadOutput> => {
+  // Keep this DOM-free for testing
+  provideCompletionItems({ prefix, wrapperClasses, text, value }: TypeaheadInput, context?: any): TypeaheadOutput {
     // Local text properties
     const empty = value.document.text.length === 0;
-    const selectedLines = value.document.getTextsAtRange(value.selection);
-    const currentLine = selectedLines.size === 1 ? selectedLines.first().getText() : null;
-
-    const nextCharacter = currentLine ? currentLine[value.selection.anchor.offset] : null;
+    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 follow a complete expression and has no text after it
+    // 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 operatorsPattern = /[+\-*/^%]/;
-    const isNextOperand = text.match(operatorsPattern);
+    const isNextOperand = text.match(/[+\-*/^%]/);
 
     // Determine candidates by CSS context
-    if (wrapperClasses.includes('context-range')) {
+    if (_.includes(wrapperClasses, 'context-range')) {
       // Suggestions for metric[|]
       return this.getRangeCompletionItems();
-    } else if (wrapperClasses.includes('context-labels')) {
+    } else if (_.includes(wrapperClasses, 'context-labels')) {
       // Suggestions for metric{|} and metric{foo=|}, as well as metric-independent label queries like {|}
-      return this.getLabelCompletionItems({ prefix, text, value, labelKey, wrapperClasses });
-    } else if (wrapperClasses.includes('context-aggregation')) {
+      return this.getLabelCompletionItems.apply(this, arguments);
+    } else if (_.includes(wrapperClasses, 'context-aggregation')) {
       // Suggestions for sum(metric) by (|)
-      return this.getAggregationCompletionItems({ prefix, text, value, labelKey, wrapperClasses });
+      return this.getAggregationCompletionItems.apply(this, arguments);
     } else if (empty) {
       // Suggestions for empty query field
-      return this.getEmptyCompletionItems(context);
-    } else if ((prefixUnrecognized && noSuffix) || safeEmptyPrefix || isNextOperand) {
+      return this.getEmptyCompletionItems(context || {});
+    } else if (prefixUnrecognized || safeEmptyPrefix || isNextOperand) {
       // Show term suggestions in a couple of scenarios
       return this.getTermCompletionItems();
     }
@@ -162,20 +146,20 @@ export default class PromQlLanguageProvider extends LanguageProvider {
     return {
       suggestions: [],
     };
-  };
+  }
 
-  getEmptyCompletionItems = (context: { history: Array<HistoryItem<PromQuery>> }): TypeaheadOutput => {
+  getEmptyCompletionItems(context: any): TypeaheadOutput {
     const { history } = context;
-    const suggestions = [];
+    let suggestions: CompletionItemGroup[] = [];
 
-    if (history && history.length) {
+    if (history && history.length > 0) {
       const historyItems = _.chain(history)
-        .map(h => h.query.expr)
+        .map((h: any) => h.query.expr)
         .filter()
         .uniq()
         .take(HISTORY_ITEM_COUNT)
         .map(wrapLabel)
-        .map(item => addHistoryMetadata(item, history))
+        .map((item: CompletionItem) => addHistoryMetadata(item, history))
         .value();
 
       suggestions.push({
@@ -187,14 +171,14 @@ export default class PromQlLanguageProvider extends LanguageProvider {
     }
 
     const termCompletionItems = this.getTermCompletionItems();
-    suggestions.push(...termCompletionItems.suggestions);
+    suggestions = [...suggestions, ...termCompletionItems.suggestions];
 
     return { suggestions };
-  };
+  }
 
-  getTermCompletionItems = (): TypeaheadOutput => {
+  getTermCompletionItems(): TypeaheadOutput {
     const { metrics } = this;
-    const suggestions = [];
+    const suggestions: CompletionItemGroup[] = [];
 
     suggestions.push({
       prefixMatch: true,
@@ -202,15 +186,14 @@ export default class PromQlLanguageProvider extends LanguageProvider {
       items: FUNCTIONS.map(setFunctionKind),
     });
 
-    if (metrics && metrics.length) {
+    if (metrics && metrics.length > 0) {
       suggestions.push({
         label: 'Metrics',
         items: metrics.map(wrapLabel),
       });
     }
-
     return { suggestions };
-  };
+  }
 
   getRangeCompletionItems(): TypeaheadOutput {
     return {
@@ -236,21 +219,21 @@ export default class PromQlLanguageProvider extends LanguageProvider {
     );
   }
 
-  getAggregationCompletionItems = ({ value }: TypeaheadInput): TypeaheadOutput => {
+  getAggregationCompletionItems({ value }: TypeaheadInput): TypeaheadOutput {
     const refresher: Promise<any> = null;
     const suggestions: CompletionItemGroup[] = [];
 
     // Stitch all query lines together to support multi-line queries
     let queryOffset;
-    const queryText = value.document.getBlocks().reduce((text: string, block) => {
+    const queryText = value.document.getBlocks().reduce((text: string, block: any) => {
       const blockText = block.getText();
       if (value.anchorBlock.key === block.key) {
         // Newline characters are not accounted for but this is irrelevant
         // for the purpose of extracting the selector string
-        queryOffset = value.selection.anchor.offset + text.length;
+        queryOffset = value.anchorOffset + text.length;
       }
-
-      return text + blockText;
+      text += blockText;
+      return text;
     }, '');
 
     // Try search for selector part on the left-hand side, such as `sum (m) by (l)`
@@ -276,10 +259,10 @@ export default class PromQlLanguageProvider extends LanguageProvider {
       return result;
     }
 
+    let selectorString = queryText.slice(openParensSelectorIndex + 1, closeParensSelectorIndex);
+
     // Range vector syntax not accounted for by subsequent parse so discard it if present
-    const selectorString = queryText
-      .slice(openParensSelectorIndex + 1, closeParensSelectorIndex)
-      .replace(/\[[^\]]+\]$/, '');
+    selectorString = selectorString.replace(/\[[^\]]+\]$/, '');
 
     const selector = parseSelector(selectorString, selectorString.length - 2).selector;
 
@@ -291,16 +274,14 @@ export default class PromQlLanguageProvider extends LanguageProvider {
     }
 
     return result;
-  };
+  }
 
-  getLabelCompletionItems = async ({
-    text,
-    wrapperClasses,
-    labelKey,
-    value,
-  }: TypeaheadInput): Promise<TypeaheadOutput> => {
+  getLabelCompletionItems({ text, wrapperClasses, labelKey, value }: TypeaheadInput): TypeaheadOutput {
+    let context: string;
+    let refresher: Promise<any> = null;
+    const suggestions: CompletionItemGroup[] = [];
     const line = value.anchorBlock.getText();
-    const cursorOffset = value.selection.anchor.offset;
+    const cursorOffset: number = value.anchorOffset;
 
     // Get normalized selector
     let selector;
@@ -311,23 +292,10 @@ export default class PromQlLanguageProvider extends LanguageProvider {
     } catch {
       selector = EMPTY_SELECTOR;
     }
-
-    const containsMetric = selector.includes('__name__=');
+    const containsMetric = selector.indexOf('__name__=') > -1;
     const existingKeys = parsedSelector ? parsedSelector.labelKeys : [];
 
-    // Query labels for selector
-    if (selector && (!this.labelValues[selector] || this.timeRangeChanged())) {
-      if (selector === EMPTY_SELECTOR) {
-        // Query label values for default labels
-        await Promise.all(DEFAULT_KEYS.map(key => this.fetchLabelValues(key)));
-      } else {
-        await this.fetchSeriesLabels(selector, !containsMetric);
-      }
-    }
-
-    const suggestions = [];
-    let context: string;
-    if ((text && text.match(/^!?=~?/)) || wrapperClasses.includes('attr-value')) {
+    if ((text && text.match(/^!?=~?/)) || _.includes(wrapperClasses, 'attr-value')) {
       // Label values
       if (labelKey && this.labelValues[selector] && this.labelValues[selector][labelKey]) {
         const labelValues = this.labelValues[selector][labelKey];
@@ -340,20 +308,27 @@ export default class PromQlLanguageProvider extends LanguageProvider {
     } else {
       // Label keys
       const labelKeys = this.labelKeys[selector] || (containsMetric ? null : DEFAULT_KEYS);
-
       if (labelKeys) {
         const possibleKeys = _.difference(labelKeys, existingKeys);
-        if (possibleKeys.length) {
+        if (possibleKeys.length > 0) {
           context = 'context-labels';
-          const newItems = possibleKeys.map(key => ({ label: key }));
-          const newSuggestion: CompletionItemGroup = { label: `Labels`, items: newItems };
-          suggestions.push(newSuggestion);
+          suggestions.push({ label: `Labels`, items: possibleKeys.map(wrapLabel) });
         }
       }
     }
 
-    return { context, suggestions };
-  };
+    // Query labels for selector
+    if (selector && (!this.labelValues[selector] || this.timeRangeChanged())) {
+      if (selector === EMPTY_SELECTOR) {
+        // Query label values for default labels
+        refresher = Promise.all(DEFAULT_KEYS.map(key => this.fetchLabelValues(key)));
+      } else {
+        refresher = this.fetchSeriesLabels(selector, !containsMetric);
+      }
+    }
+
+    return { context, refresher, suggestions };
+  }
 
   fetchLabelValues = async (key: string) => {
     try {

+ 4 - 4
public/app/plugins/datasource/prometheus/language_utils.ts

@@ -16,13 +16,13 @@ export const processHistogramLabels = (labels: string[]) => {
   return { values: { __name__: result } };
 };
 
-export function processLabels(labels: Array<{ [key: string]: string }>, withName = false) {
+export function processLabels(labels: any, withName = false) {
   const values: { [key: string]: string[] } = {};
-  labels.forEach(l => {
+  labels.forEach((l: any) => {
     const { __name__, ...rest } = l;
     if (withName) {
       values['__name__'] = values['__name__'] || [];
-      if (!values['__name__'].includes(__name__)) {
+      if (values['__name__'].indexOf(__name__) === -1) {
         values['__name__'].push(__name__);
       }
     }
@@ -31,7 +31,7 @@ export function processLabels(labels: Array<{ [key: string]: string }>, withName
       if (!values[key]) {
         values[key] = [];
       }
-      if (!values[key].includes(rest[key])) {
+      if (values[key].indexOf(rest[key]) === -1) {
         values[key].push(rest[key]);
       }
     });

+ 111 - 111
public/app/plugins/datasource/prometheus/specs/language_provider.test.ts

@@ -1,22 +1,21 @@
+// @ts-ignore
 import Plain from 'slate-plain-serializer';
-import { Editor as SlateEditor } from 'slate';
+
 import LanguageProvider from '../language_provider';
-import { PrometheusDatasource } from '../datasource';
-import { HistoryItem } from 'app/types';
-import { PromQuery } from '../types';
 
 describe('Language completion provider', () => {
-  const datasource: PrometheusDatasource = ({
+  const datasource = {
     metadataRequest: () => ({ data: { data: [] as any[] } }),
     getTimeRange: () => ({ start: 0, end: 1 }),
-  } as any) as PrometheusDatasource;
+  };
 
   describe('empty query suggestions', () => {
-    it('returns default suggestions on empty context', async () => {
+    it('returns default suggestions on emtpty context', () => {
       const instance = new LanguageProvider(datasource);
       const value = Plain.deserialize('');
-      const result = await instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] });
+      const result = instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] });
       expect(result.context).toBeUndefined();
+      expect(result.refresher).toBeUndefined();
       expect(result.suggestions).toMatchObject([
         {
           label: 'Functions',
@@ -24,11 +23,12 @@ describe('Language completion provider', () => {
       ]);
     });
 
-    it('returns default suggestions with metrics on empty context when metrics were provided', async () => {
+    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 = await instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] });
+      const result = instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] });
       expect(result.context).toBeUndefined();
+      expect(result.refresher).toBeUndefined();
       expect(result.suggestions).toMatchObject([
         {
           label: 'Functions',
@@ -39,21 +39,17 @@ describe('Language completion provider', () => {
       ]);
     });
 
-    it('returns default suggestions with history on empty context when history was provided', async () => {
+    it('returns default suggestions with history on emtpty context when history was provided', () => {
       const instance = new LanguageProvider(datasource);
       const value = Plain.deserialize('');
-      const history: Array<HistoryItem<PromQuery>> = [
+      const history = [
         {
-          ts: 0,
           query: { refId: '1', expr: 'metric' },
         },
       ];
-      const result = await instance.provideCompletionItems(
-        { text: '', prefix: '', value, wrapperClasses: [] },
-        { history }
-      );
+      const result = instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] }, { history });
       expect(result.context).toBeUndefined();
-
+      expect(result.refresher).toBeUndefined();
       expect(result.suggestions).toMatchObject([
         {
           label: 'History',
@@ -71,16 +67,17 @@ describe('Language completion provider', () => {
   });
 
   describe('range suggestions', () => {
-    it('returns range suggestions in range context', async () => {
+    it('returns range suggestions in range context', () => {
       const instance = new LanguageProvider(datasource);
       const value = Plain.deserialize('1');
-      const result = await instance.provideCompletionItems({
+      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).toMatchObject([
         {
           items: [
@@ -99,12 +96,12 @@ describe('Language completion provider', () => {
   });
 
   describe('metric suggestions', () => {
-    it('returns metrics and function suggestions in an unknown context', async () => {
+    it('returns metrics and function suggestions in an unknown context', () => {
       const instance = new LanguageProvider(datasource, { metrics: ['foo', 'bar'] });
-      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 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',
@@ -115,11 +112,12 @@ describe('Language completion provider', () => {
       ]);
     });
 
-    it('returns metrics and function  suggestions after a binary operator', async () => {
+    it('returns metrics and function  suggestions after a binary operator', () => {
       const instance = new LanguageProvider(datasource, { metrics: ['foo', 'bar'] });
       const value = Plain.deserialize('*');
-      const result = await instance.provideCompletionItems({ text: '*', prefix: '', value, wrapperClasses: [] });
+      const result = instance.provideCompletionItems({ text: '*', prefix: '', value, wrapperClasses: [] });
       expect(result.context).toBeUndefined();
+      expect(result.refresher).toBeUndefined();
       expect(result.suggestions).toMatchObject([
         {
           label: 'Functions',
@@ -130,30 +128,34 @@ describe('Language completion provider', () => {
       ]);
     });
 
-    it('returns no suggestions at the beginning of a non-empty function', async () => {
+    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 ed = new SlateEditor({ value });
-
-      const valueWithSelection = ed.moveForward(4).value;
-      const result = await instance.provideCompletionItems({
+      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);
     });
   });
 
   describe('label suggestions', () => {
-    it('returns default label suggestions on label context and no metric', async () => {
+    it('returns default label suggestions on label context and no metric', () => {
       const instance = new LanguageProvider(datasource);
       const value = Plain.deserialize('{}');
-      const ed = new SlateEditor({ value });
-      const valueWithSelection = ed.moveForward(1).value;
-      const result = await instance.provideCompletionItems({
+      const range = value.selection.merge({
+        anchorOffset: 1,
+      });
+      const valueWithSelection = value.change().select(range).value;
+      const result = instance.provideCompletionItems({
         text: '',
         prefix: '',
         wrapperClasses: ['context-labels'],
@@ -163,16 +165,14 @@ describe('Language completion provider', () => {
       expect(result.suggestions).toEqual([{ items: [{ label: 'job' }, { label: 'instance' }], label: 'Labels' }]);
     });
 
-    it('returns label suggestions on label context and metric', async () => {
-      const datasources: PrometheusDatasource = ({
-        metadataRequest: () => ({ data: { data: [{ __name__: 'metric', bar: 'bazinga' }] as any[] } }),
-        getTimeRange: () => ({ start: 0, end: 1 }),
-      } as any) as PrometheusDatasource;
-      const instance = new LanguageProvider(datasources, { labelKeys: { '{__name__="metric"}': ['bar'] } });
+    it('returns label suggestions on label context and metric', () => {
+      const instance = new LanguageProvider(datasource, { labelKeys: { '{__name__="metric"}': ['bar'] } });
       const value = Plain.deserialize('metric{}');
-      const ed = new SlateEditor({ value });
-      const valueWithSelection = ed.moveForward(7).value;
-      const result = await instance.provideCompletionItems({
+      const range = value.selection.merge({
+        anchorOffset: 7,
+      });
+      const valueWithSelection = value.change().select(range).value;
+      const result = instance.provideCompletionItems({
         text: '',
         prefix: '',
         wrapperClasses: ['context-labels'],
@@ -182,32 +182,16 @@ describe('Language completion provider', () => {
       expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]);
     });
 
-    it('returns label suggestions on label context but leaves out labels that already exist', async () => {
-      const datasources: PrometheusDatasource = ({
-        metadataRequest: () => ({
-          data: {
-            data: [
-              {
-                __name__: 'metric',
-                bar: 'asdasd',
-                job1: 'dsadsads',
-                job2: 'fsfsdfds',
-                job3: 'dsadsad',
-              },
-            ],
-          },
-        }),
-        getTimeRange: () => ({ start: 0, end: 1 }),
-      } as any) as PrometheusDatasource;
-      const instance = new LanguageProvider(datasources, {
-        labelKeys: {
-          '{job1="foo",job2!="foo",job3=~"foo",__name__="metric"}': ['bar', 'job1', 'job2', 'job3', '__name__'],
-        },
+    it('returns label suggestions on label context but leaves out labels that already exist', () => {
+      const instance = new LanguageProvider(datasource, {
+        labelKeys: { '{job1="foo",job2!="foo",job3=~"foo"}': ['bar', 'job1', 'job2', 'job3'] },
       });
-      const value = Plain.deserialize('{job1="foo",job2!="foo",job3=~"foo",__name__="metric",}');
-      const ed = new SlateEditor({ value });
-      const valueWithSelection = ed.moveForward(54).value;
-      const result = await instance.provideCompletionItems({
+      const value = Plain.deserialize('{job1="foo",job2!="foo",job3=~"foo",}');
+      const range = value.selection.merge({
+        anchorOffset: 36,
+      });
+      const valueWithSelection = value.change().select(range).value;
+      const result = instance.provideCompletionItems({
         text: '',
         prefix: '',
         wrapperClasses: ['context-labels'],
@@ -217,15 +201,15 @@ describe('Language completion provider', () => {
       expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]);
     });
 
-    it('returns label value suggestions inside a label value context after a negated matching operator', async () => {
+    it('returns label value suggestions inside a label value context after a negated matching operator', () => {
       const instance = new LanguageProvider(datasource, {
         labelKeys: { '{}': ['label'] },
         labelValues: { '{}': { label: ['a', 'b', 'c'] } },
       });
       const value = Plain.deserialize('{label!=}');
-      const ed = new SlateEditor({ value });
-      const valueWithSelection = ed.moveForward(8).value;
-      const result = await instance.provideCompletionItems({
+      const range = value.selection.merge({ anchorOffset: 8 });
+      const valueWithSelection = value.change().select(range).value;
+      const result = instance.provideCompletionItems({
         text: '!=',
         prefix: '',
         wrapperClasses: ['context-labels'],
@@ -241,30 +225,35 @@ describe('Language completion provider', () => {
       ]);
     });
 
-    it('returns a refresher on label context and unavailable metric', async () => {
+    it('returns a refresher on label context and unavailable metric', () => {
       const instance = new LanguageProvider(datasource, { labelKeys: { '{__name__="foo"}': ['bar'] } });
       const value = Plain.deserialize('metric{}');
-      const ed = new SlateEditor({ value });
-      const valueWithSelection = ed.moveForward(7).value;
-      const result = await instance.provideCompletionItems({
+      const range = value.selection.merge({
+        anchorOffset: 7,
+      });
+      const valueWithSelection = value.change().select(range).value;
+      const result = instance.provideCompletionItems({
         text: '',
         prefix: '',
         wrapperClasses: ['context-labels'],
         value: valueWithSelection,
       });
       expect(result.context).toBeUndefined();
+      expect(result.refresher).toBeInstanceOf(Promise);
       expect(result.suggestions).toEqual([]);
     });
 
-    it('returns label values on label context when given a metric and a label key', async () => {
+    it('returns label values on label context when given a metric and a label key', () => {
       const instance = new LanguageProvider(datasource, {
         labelKeys: { '{__name__="metric"}': ['bar'] },
         labelValues: { '{__name__="metric"}': { bar: ['baz'] } },
       });
       const value = Plain.deserialize('metric{bar=ba}');
-      const ed = new SlateEditor({ value });
-      const valueWithSelection = ed.moveForward(13).value;
-      const result = await instance.provideCompletionItems({
+      const range = value.selection.merge({
+        anchorOffset: 13,
+      });
+      const valueWithSelection = value.change().select(range).value;
+      const result = instance.provideCompletionItems({
         text: '=ba',
         prefix: 'ba',
         wrapperClasses: ['context-labels'],
@@ -275,12 +264,14 @@ describe('Language completion provider', () => {
       expect(result.suggestions).toEqual([{ items: [{ label: 'baz' }], label: 'Label values for "bar"' }]);
     });
 
-    it('returns label suggestions on aggregation context and metric w/ selector', async () => {
+    it('returns label suggestions on aggregation context and metric w/ selector', () => {
       const instance = new LanguageProvider(datasource, { labelKeys: { '{__name__="metric",foo="xx"}': ['bar'] } });
       const value = Plain.deserialize('sum(metric{foo="xx"}) by ()');
-      const ed = new SlateEditor({ value });
-      const valueWithSelection = ed.moveForward(26).value;
-      const result = await instance.provideCompletionItems({
+      const range = value.selection.merge({
+        anchorOffset: 26,
+      });
+      const valueWithSelection = value.change().select(range).value;
+      const result = instance.provideCompletionItems({
         text: '',
         prefix: '',
         wrapperClasses: ['context-aggregation'],
@@ -290,12 +281,14 @@ describe('Language completion provider', () => {
       expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]);
     });
 
-    it('returns label suggestions on aggregation context and metric w/o selector', async () => {
+    it('returns label suggestions on aggregation context and metric w/o selector', () => {
       const instance = new LanguageProvider(datasource, { labelKeys: { '{__name__="metric"}': ['bar'] } });
       const value = Plain.deserialize('sum(metric) by ()');
-      const ed = new SlateEditor({ value });
-      const valueWithSelection = ed.moveForward(16).value;
-      const result = await instance.provideCompletionItems({
+      const range = value.selection.merge({
+        anchorOffset: 16,
+      });
+      const valueWithSelection = value.change().select(range).value;
+      const result = instance.provideCompletionItems({
         text: '',
         prefix: '',
         wrapperClasses: ['context-aggregation'],
@@ -305,16 +298,15 @@ describe('Language completion provider', () => {
       expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]);
     });
 
-    it('returns label suggestions inside a multi-line aggregation context', async () => {
+    it('returns label suggestions inside a multi-line aggregation context', () => {
       const instance = new LanguageProvider(datasource, {
         labelKeys: { '{__name__="metric"}': ['label1', 'label2', 'label3'] },
       });
       const value = Plain.deserialize('sum(\nmetric\n)\nby ()');
-      const aggregationTextBlock = value.document.getBlocks().get(3);
-      const ed = new SlateEditor({ value });
-      ed.moveToStartOfNode(aggregationTextBlock);
-      const valueWithSelection = ed.moveForward(4).value;
-      const result = await instance.provideCompletionItems({
+      const aggregationTextBlock = value.document.getBlocksAsArray()[3];
+      const range = value.selection.moveToStartOf(aggregationTextBlock).merge({ anchorOffset: 4 });
+      const valueWithSelection = value.change().select(range).value;
+      const result = instance.provideCompletionItems({
         text: '',
         prefix: '',
         wrapperClasses: ['context-aggregation'],
@@ -329,14 +321,16 @@ describe('Language completion provider', () => {
       ]);
     });
 
-    it('returns label suggestions inside an aggregation context with a range vector', async () => {
+    it('returns label suggestions inside an aggregation context with a range vector', () => {
       const instance = new LanguageProvider(datasource, {
         labelKeys: { '{__name__="metric"}': ['label1', 'label2', 'label3'] },
       });
       const value = Plain.deserialize('sum(rate(metric[1h])) by ()');
-      const ed = new SlateEditor({ value });
-      const valueWithSelection = ed.moveForward(26).value;
-      const result = await instance.provideCompletionItems({
+      const range = value.selection.merge({
+        anchorOffset: 26,
+      });
+      const valueWithSelection = value.change().select(range).value;
+      const result = instance.provideCompletionItems({
         text: '',
         prefix: '',
         wrapperClasses: ['context-aggregation'],
@@ -351,14 +345,16 @@ describe('Language completion provider', () => {
       ]);
     });
 
-    it('returns label suggestions inside an aggregation context with a range vector and label', async () => {
+    it('returns label suggestions inside an aggregation context with a range vector and label', () => {
       const instance = new LanguageProvider(datasource, {
         labelKeys: { '{__name__="metric",label1="value"}': ['label1', 'label2', 'label3'] },
       });
       const value = Plain.deserialize('sum(rate(metric{label1="value"}[1h])) by ()');
-      const ed = new SlateEditor({ value });
-      const valueWithSelection = ed.moveForward(42).value;
-      const result = await instance.provideCompletionItems({
+      const range = value.selection.merge({
+        anchorOffset: 42,
+      });
+      const valueWithSelection = value.change().select(range).value;
+      const result = instance.provideCompletionItems({
         text: '',
         prefix: '',
         wrapperClasses: ['context-aggregation'],
@@ -373,14 +369,16 @@ describe('Language completion provider', () => {
       ]);
     });
 
-    it('returns no suggestions inside an unclear aggregation context using alternate syntax', async () => {
+    it('returns no suggestions inside an unclear aggregation context using alternate syntax', () => {
       const instance = new LanguageProvider(datasource, {
         labelKeys: { '{__name__="metric"}': ['label1', 'label2', 'label3'] },
       });
       const value = Plain.deserialize('sum by ()');
-      const ed = new SlateEditor({ value });
-      const valueWithSelection = ed.moveForward(8).value;
-      const result = await instance.provideCompletionItems({
+      const range = value.selection.merge({
+        anchorOffset: 8,
+      });
+      const valueWithSelection = value.change().select(range).value;
+      const result = instance.provideCompletionItems({
         text: '',
         prefix: '',
         wrapperClasses: ['context-aggregation'],
@@ -390,14 +388,16 @@ describe('Language completion provider', () => {
       expect(result.suggestions).toEqual([]);
     });
 
-    it('returns label suggestions inside an aggregation context using alternate syntax', async () => {
+    it('returns label suggestions inside an aggregation context using alternate syntax', () => {
       const instance = new LanguageProvider(datasource, {
         labelKeys: { '{__name__="metric"}': ['label1', 'label2', 'label3'] },
       });
       const value = Plain.deserialize('sum by () (metric)');
-      const ed = new SlateEditor({ value });
-      const valueWithSelection = ed.moveForward(8).value;
-      const result = await instance.provideCompletionItems({
+      const range = value.selection.merge({
+        anchorOffset: 8,
+      });
+      const valueWithSelection = value.change().select(range).value;
+      const result = instance.provideCompletionItems({
         text: '',
         prefix: '',
         wrapperClasses: ['context-aggregation'],

+ 7 - 25
public/app/types/explore.ts

@@ -23,18 +23,11 @@ import {
 import { Emitter } from 'app/core/core';
 import TableModel from 'app/core/table_model';
 
-import { Value } from 'slate';
-
-import { Editor } from '@grafana/slate-react';
 export enum ExploreMode {
   Metrics = 'Metrics',
   Logs = 'Logs',
 }
 
-export enum CompletionItemKind {
-  GroupTitle = 'GroupTitle',
-}
-
 export interface CompletionItem {
   /**
    * The label of this completion item. By default
@@ -42,48 +35,40 @@ export interface CompletionItem {
    * this completion.
    */
   label: string;
-
   /**
-   * The kind of this completion item. An icon is chosen
-   * by the editor based on the kind.
+   * The kind of this completion item. Based on the kind
+   * an icon is chosen by the editor.
    */
-  kind?: CompletionItemKind | string;
-
+  kind?: string;
   /**
    * A human-readable string with additional information
    * about this item, like type or symbol information.
    */
   detail?: string;
-
   /**
    * A human-readable string, can be Markdown, that represents a doc-comment.
    */
   documentation?: string;
-
   /**
    * A string that should be used when comparing this item
    * with other items. When `falsy` the `label` is used.
    */
   sortText?: string;
-
   /**
    * A string that should be used when filtering a set of
    * completion items. When `falsy` the `label` is used.
    */
   filterText?: string;
-
   /**
    * A string or snippet that should be inserted in a document when selecting
    * this completion. When `falsy` the `label` is used.
    */
   insertText?: string;
-
   /**
    * Delete number of characters before the caret position,
    * by default the letters from the beginning of the word.
    */
   deleteBackwards?: number;
-
   /**
    * Number of steps to move after the insertion, can be negative.
    */
@@ -95,22 +80,18 @@ export interface CompletionItemGroup {
    * Label that will be displayed for all entries of this group.
    */
   label: string;
-
   /**
    * List of suggestions of this group.
    */
   items: CompletionItem[];
-
   /**
    * If true, match only by prefix (and not mid-word).
    */
   prefixMatch?: boolean;
-
   /**
    * If true, do not filter items in this group based on the search.
    */
   skipFilter?: boolean;
-
   /**
    * If true, do not sort items.
    */
@@ -313,7 +294,7 @@ export interface HistoryItem<TQuery extends DataQuery = DataQuery> {
 }
 
 export abstract class LanguageProvider {
-  datasource: DataSourceApi;
+  datasource: any;
   request: (url: string, params?: any) => Promise<any>;
   /**
    * Returns startTask that resolves with a task list when main syntax is loaded.
@@ -328,12 +309,13 @@ export interface TypeaheadInput {
   prefix: string;
   wrapperClasses: string[];
   labelKey?: string;
-  value?: Value;
-  editor?: Editor;
+  //Should be Value from slate
+  value?: any;
 }
 
 export interface TypeaheadOutput {
   context?: string;
+  refresher?: Promise<{}>;
   suggestions: CompletionItemGroup[];
 }
 

+ 4 - 4
public/sass/components/_slate_editor.scss

@@ -30,9 +30,9 @@
   .typeahead {
     position: absolute;
     z-index: auto;
-    top: 100px;
-    left: 160px;
-    //opacity: 0;
+    top: -10000px;
+    left: -10000px;
+    opacity: 0;
     border-radius: $border-radius;
     border: $panel-border;
     max-height: calc(66vh);
@@ -43,7 +43,7 @@
     list-style: none;
     background: $panel-bg;
     color: $text-color;
-    //transition: opacity 0.4s ease-out;
+    transition: opacity 0.4s ease-out;
     box-shadow: $typeahead-shadow;
   }
 

+ 1 - 2
tsconfig.json

@@ -31,8 +31,7 @@
     "typeRoots": ["node_modules/@types", "public/app/types"],
     "paths": {
       "app": ["app"],
-      "sass": ["sass"],
-      "@grafana/slate-react": ["../node_modules/@types/slate-react"]
+      "sass": ["sass"]
     },
     "skipLibCheck": true,
     "preserveSymlinks": true

+ 109 - 111
yarn.lock

@@ -1227,28 +1227,6 @@
     unique-filename "^1.1.1"
     which "^1.3.1"
 
-"@grafana/slate-react@0.22.9-grafana":
-  version "0.22.9-grafana"
-  resolved "https://registry.yarnpkg.com/@grafana/slate-react/-/slate-react-0.22.9-grafana.tgz#07f35f0ffc018f616b9f82fa6e5ba65fae75c6a0"
-  integrity sha512-9NYjwabVOUQ/e4Y/Wm+sgePM65rb/gju59D52t4O42HsIm9exXv+SLajEBF/HiLHzuH5V+5uuHajbzv0vuE2VA==
-  dependencies:
-    debug "^3.1.0"
-    get-window "^1.1.1"
-    is-window "^1.0.2"
-    lodash "^4.1.1"
-    memoize-one "^4.0.0"
-    prop-types "^15.5.8"
-    react-immutable-proptypes "^2.1.0"
-    selection-is-backward "^1.0.0"
-    slate-base64-serializer "^0.2.111"
-    slate-dev-environment "^0.2.2"
-    slate-hotkeys "^0.2.9"
-    slate-plain-serializer "^0.7.10"
-    slate-prop-types "^0.5.41"
-    slate-react-placeholder "^0.2.8"
-    tiny-invariant "^1.0.1"
-    tiny-warning "^0.0.3"
-
 "@icons/material@^0.2.4":
   version "0.2.4"
   resolved "https://registry.yarnpkg.com/@icons/material/-/material-0.2.4.tgz#e90c9f71768b3736e76d7dd6783fc6c2afa88bc8"
@@ -3430,26 +3408,10 @@
   version "7.0.11"
   resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-7.0.11.tgz#6f28f005a36e779b7db0f1359b9fb9eef72aae88"
 
-"@types/slate-plain-serializer@0.6.1":
-  version "0.6.1"
-  resolved "https://registry.yarnpkg.com/@types/slate-plain-serializer/-/slate-plain-serializer-0.6.1.tgz#c392ce51621f7c55df0976f161dcfca18bd559ee"
-  integrity sha512-5meyKFvmWH1T02j2dbAaY8kn/FNofxP79jV3TsfuLsUIeHkON5CroBxAyrgkYF4vHp+MVWZddI36Yvwl7Y0Feg==
-  dependencies:
-    "@types/slate" "*"
-
-"@types/slate-react@0.22.5":
-  version "0.22.5"
-  resolved "https://registry.yarnpkg.com/@types/slate-react/-/slate-react-0.22.5.tgz#a10796758aa6b3133e1c777959facbf8806959f7"
-  integrity sha512-WKJic5LlNRNUCnD6lEdlOZCcXWoDN8Ais2CmwVMn8pdt5Kh8hJsTYhXawNxOShPIOLVB+G+aVZNAXAAubEOpaw==
-  dependencies:
-    "@types/react" "*"
-    "@types/slate" "*"
-    immutable "^3.8.2"
-
-"@types/slate@*", "@types/slate@0.47.1":
-  version "0.47.1"
-  resolved "https://registry.yarnpkg.com/@types/slate/-/slate-0.47.1.tgz#6c66f82df085c764039eea2229be763f7e1906fd"
-  integrity sha512-2ZlnWI6/RYMXxeGFIeZtvmaXAeYAJh4ZVumziqVl77/liNEi9hOwkUTU2zFu+j/z21v385I2WVPl8sgadxfzXg==
+"@types/slate@0.44.11":
+  version "0.44.11"
+  resolved "https://registry.yarnpkg.com/@types/slate/-/slate-0.44.11.tgz#152568096d1a089fa4c5bb03de1cf044a377206c"
+  integrity sha512-UnOGipgkE1+rq3L4JjsTO0b02FbT6b59+0/hkW/QFBDvCcxCSAdwdr9HYjXkMSCSVlcsEfdC/cz+XOaB+tGvlg==
   dependencies:
     "@types/react" "*"
     immutable "^3.8.2"
@@ -4713,6 +4675,7 @@ bail@^1.0.0:
 balanced-match@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
+  integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
 
 baron@3.0.3:
   version "3.0.3"
@@ -4871,6 +4834,7 @@ boxen@^2.1.0:
 brace-expansion@^1.0.0, brace-expansion@^1.1.7:
   version "1.1.11"
   resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
+  integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
   dependencies:
     balanced-match "^1.0.0"
     concat-map "0.0.1"
@@ -5759,6 +5723,7 @@ compression@^1.5.2:
 concat-map@0.0.1:
   version "0.0.1"
   resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
+  integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
 
 concat-stream@1.6.2, concat-stream@^1.4.6, concat-stream@^1.5.0:
   version "1.6.2"
@@ -7189,7 +7154,6 @@ dir-glob@^2.0.0:
 direction@^0.1.5:
   version "0.1.5"
   resolved "https://registry.yarnpkg.com/direction/-/direction-0.1.5.tgz#ce5d797f97e26f8be7beff53f7dc40e1c1a9ec4c"
-  integrity sha1-zl15f5fib4vnvv9T99xA4cGp7Ew=
 
 discontinuous-range@1.0.0:
   version "1.0.0"
@@ -7781,7 +7745,6 @@ esrecurse@^4.1.0:
 esrever@^0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/esrever/-/esrever-0.2.0.tgz#96e9d28f4f1b1a76784cd5d490eaae010e7407b8"
-  integrity sha1-lunSj08bGnZ4TNXUkOquAQ50B7g=
 
 estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0:
   version "4.2.0"
@@ -8325,6 +8288,7 @@ for-in@^0.1.3:
 for-in@^1.0.1, for-in@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
+  integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=
 
 for-own@^0.1.3, for-own@^0.1.4:
   version "0.1.5"
@@ -8485,6 +8449,7 @@ fs-write-stream-atomic@^1.0.8, fs-write-stream-atomic@~1.0.10:
 fs.realpath@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
+  integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
 
 fsevents@^1.2.7:
   version "1.2.9"
@@ -8937,8 +8902,9 @@ got@^6.7.1:
     url-parse-lax "^1.0.0"
 
 graceful-fs@^4.1.0, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9:
-  version "4.1.15"
-  resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00"
+  version "4.2.2"
+  resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.2.tgz#6f0952605d0140c1cfdb138ed005775b92d67b02"
+  integrity sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q==
 
 "graceful-readlink@>= 1.0.0":
   version "1.0.1"
@@ -9668,6 +9634,7 @@ infer-owner@^1.0.4:
 inflight@^1.0.4, inflight@~1.0.6:
   version "1.0.6"
   resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
+  integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
   dependencies:
     once "^1.3.0"
     wrappy "1"
@@ -9976,6 +9943,10 @@ is-dotfile@^1.0.0:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1"
 
+is-empty@^1.0.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/is-empty/-/is-empty-1.2.0.tgz#de9bb5b278738a05a0b09a57e1fb4d4a341a9f6b"
+
 is-equal-shallow@^0.1.3:
   version "0.1.3"
   resolved "https://registry.yarnpkg.com/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz#2238098fc221de0bcfa5d9eac4c45d638aa1c534"
@@ -9989,6 +9960,7 @@ is-extendable@^0.1.0, is-extendable@^0.1.1:
 is-extendable@^1.0.0, is-extendable@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4"
+  integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==
   dependencies:
     is-plain-object "^2.0.4"
 
@@ -10050,7 +10022,7 @@ is-hexadecimal@^1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.2.tgz#b6e710d7d07bb66b98cb8cece5c9b4921deeb835"
 
-is-hotkey@0.1.4:
+is-hotkey@0.1.4, is-hotkey@^0.1.1:
   version "0.1.4"
   resolved "https://registry.yarnpkg.com/is-hotkey/-/is-hotkey-0.1.4.tgz#c34d2c85d6ec8d09a871dcf71931c8067a824c7d"
 
@@ -10305,6 +10277,7 @@ isobject@^2.0.0:
 isobject@^3.0.0, isobject@^3.0.1:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
+  integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
 
 isobject@^4.0.0:
   version "4.0.0"
@@ -10941,7 +10914,7 @@ kew@^0.7.0:
   version "0.7.0"
   resolved "https://registry.yarnpkg.com/kew/-/kew-0.7.0.tgz#79d93d2d33363d6fdd2970b335d9141ad591d79b"
 
-keycode@^2.2.0:
+keycode@^2.1.2, keycode@^2.2.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.2.0.tgz#3d0af56dc7b8b8e5cba8d0a97f107204eec22b04"
 
@@ -11364,8 +11337,9 @@ lockfile@^1.0.4:
     signal-exit "^3.0.2"
 
 lodash-es@^4.17.11, lodash-es@^4.2.1:
-  version "4.17.11"
-  resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.11.tgz#145ab4a7ac5c5e52a3531fb4f310255a152b4be0"
+  version "4.17.15"
+  resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.15.tgz#21bd96839354412f23d7a10340e5eac6ee455d78"
+  integrity sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ==
 
 lodash._baseuniq@~4.6.0:
   version "4.6.0"
@@ -11382,7 +11356,7 @@ lodash._getnative@^3.0.0:
   version "3.9.1"
   resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5"
 
-lodash._reinterpolate@^3.0.0, lodash._reinterpolate@~3.0.0:
+lodash._reinterpolate@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
   integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=
@@ -11453,8 +11427,9 @@ lodash.memoize@^4.1.2:
   resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
 
 lodash.mergewith@^4.6.1:
-  version "4.6.1"
-  resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927"
+  version "4.6.2"
+  resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55"
+  integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==
 
 lodash.once@^4.1.1:
   version "4.1.1"
@@ -11477,7 +11452,7 @@ lodash.tail@^4.1.1:
   version "4.1.1"
   resolved "https://registry.yarnpkg.com/lodash.tail/-/lodash.tail-4.1.1.tgz#d2333a36d9e7717c8ad2f7cacafec7c32b444664"
 
-lodash.template@^4.0.2:
+lodash.template@^4.0.2, lodash.template@^4.2.4:
   version "4.5.0"
   resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.5.0.tgz#f976195cf3f347d0d5f52483569fe8031ccce8ab"
   integrity sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==
@@ -11485,20 +11460,12 @@ lodash.template@^4.0.2:
     lodash._reinterpolate "^3.0.0"
     lodash.templatesettings "^4.0.0"
 
-lodash.template@^4.2.4:
-  version "4.4.0"
-  resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.4.0.tgz#e73a0385c8355591746e020b99679c690e68fba0"
-  integrity sha1-5zoDhcg1VZF0bgILmWecaQ5o+6A=
-  dependencies:
-    lodash._reinterpolate "~3.0.0"
-    lodash.templatesettings "^4.0.0"
-
 lodash.templatesettings@^4.0.0:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-4.1.0.tgz#2b4d4e95ba440d915ff08bc899e4553666713316"
-  integrity sha1-K01OlbpEDZFf8IvImeRVNmZxMxY=
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz#e481310f049d3cf6d47e912ad09313b154f0fb33"
+  integrity sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==
   dependencies:
-    lodash._reinterpolate "~3.0.0"
+    lodash._reinterpolate "^3.0.0"
 
 lodash.throttle@^4.1.1:
   version "4.1.1"
@@ -12001,6 +11968,7 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1:
 "minimatch@2 || 3", minimatch@3.0.4, minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.4, minimatch@~3.0.0, minimatch@~3.0.2:
   version "3.0.4"
   resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
+  integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
   dependencies:
     brace-expansion "^1.1.7"
 
@@ -12021,6 +11989,7 @@ minimist-options@^3.0.1:
 minimist@0.0.8:
   version "0.0.8"
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
+  integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=
 
 minimist@1.1.x:
   version "1.1.3"
@@ -12063,8 +12032,9 @@ mississippi@^3.0.0:
     through2 "^2.0.0"
 
 mixin-deep@^1.2.0:
-  version "1.3.1"
-  resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.1.tgz#a49e7268dce1a0d9698e45326c5626df3543d0fe"
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566"
+  integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==
   dependencies:
     for-in "^1.0.2"
     is-extendable "^1.0.1"
@@ -12965,6 +12935,7 @@ on-headers@~1.0.2:
 once@^1.3.0, once@^1.3.1, once@^1.4.0, once@~1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
+  integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
   dependencies:
     wrappy "1"
 
@@ -13387,6 +13358,7 @@ path-exists@^3.0.0:
 path-is-absolute@^1.0.0, path-is-absolute@~1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
+  integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
 
 path-is-inside@^1.0.1, path-is-inside@^1.0.2, path-is-inside@~1.0.2:
   version "1.0.2"
@@ -14375,7 +14347,7 @@ pretty-hrtime@^1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1"
 
-prismjs@1.16.0, prismjs@^1.8.4, prismjs@~1.16.0:
+prismjs@1.16.0, prismjs@^1.13.0, prismjs@^1.8.4, prismjs@~1.16.0:
   version "1.16.0"
   resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.16.0.tgz#406eb2c8aacb0f5f0f1167930cb83835d10a4308"
   optionalDependencies:
@@ -15100,6 +15072,12 @@ react-popper@^1.3.3:
     typed-styles "^0.0.7"
     warning "^4.0.2"
 
+react-portal@^3.1.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/react-portal/-/react-portal-3.2.0.tgz#4224e19b2b05d5cbe730a7ba0e34ec7585de0043"
+  dependencies:
+    prop-types "^15.5.8"
+
 react-redux@5.1.1:
   version "5.1.1"
   resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.1.1.tgz#88e368682c7fa80e34e055cd7ac56f5936b0f52f"
@@ -16507,55 +16485,81 @@ slash@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44"
 
-slate-base64-serializer@^0.2.111:
-  version "0.2.111"
-  resolved "https://registry.yarnpkg.com/slate-base64-serializer/-/slate-base64-serializer-0.2.111.tgz#22ba7d32aa4650f6bbd25c26ffe11f5d021959d6"
-  integrity sha512-pEsbxz4msVSCCCkn7rX+lHXxUj/oddcR4VsIYwWeQQLm9Uw7Ovxja4rQ/hVFcQqoU2DIjITRwBR9pv3RyS+PZQ==
+slate-base64-serializer@^0.2.36:
+  version "0.2.102"
+  resolved "https://registry.yarnpkg.com/slate-base64-serializer/-/slate-base64-serializer-0.2.102.tgz#05cdb9149172944b55c8d0a0d14b4499a1c3b5a2"
   dependencies:
     isomorphic-base64 "^1.0.2"
 
-slate-dev-environment@^0.2.2:
-  version "0.2.2"
-  resolved "https://registry.yarnpkg.com/slate-dev-environment/-/slate-dev-environment-0.2.2.tgz#bd8946e1fe4cf5447060c84a362a1d026ed8b77f"
-  integrity sha512-JZ09llrRQu6JUsLJCUlGC0lB1r1qIAabAkSd454iyYBq6lDuY//Bypi3Jo8yzIfzZ4+mRLdQvl9e8MbeM9l48Q==
+slate-dev-environment@^0.1.2, slate-dev-environment@^0.1.4:
+  version "0.1.6"
+  resolved "https://registry.yarnpkg.com/slate-dev-environment/-/slate-dev-environment-0.1.6.tgz#ff22b40ef4cc890ff7706b6b657abc276782424f"
   dependencies:
     is-in-browser "^1.1.3"
 
-slate-hotkeys@^0.2.9:
-  version "0.2.9"
-  resolved "https://registry.yarnpkg.com/slate-hotkeys/-/slate-hotkeys-0.2.9.tgz#0cc9eb750a49ab9ef11601305b7c82b5402348e3"
-  integrity sha512-y+C/s5vJEmBxo8fIqHmUcdViGwALL/A6Qow3sNG1OHYD5SI11tC2gfYtGbPh+2q0H7O4lufffCmFsP5bMaDHqA==
-  dependencies:
-    is-hotkey "0.1.4"
-    slate-dev-environment "^0.2.2"
+slate-dev-logger@^0.1.39, slate-dev-logger@^0.1.43:
+  version "0.1.43"
+  resolved "https://registry.yarnpkg.com/slate-dev-logger/-/slate-dev-logger-0.1.43.tgz#77f6ca7207fcbf453a5516f3aa8b19794d1d26dc"
 
-slate-plain-serializer@0.7.10, slate-plain-serializer@^0.7.10:
-  version "0.7.10"
-  resolved "https://registry.yarnpkg.com/slate-plain-serializer/-/slate-plain-serializer-0.7.10.tgz#bc4a6942cf52fde826019bb1095dffd0dac8cc08"
-  integrity sha512-/QvMCQ0F3NzbnuoW+bxsLIChPdRgxBjQeGhYhpRGTVvlZCLOmfDvavhN6fHsuEwkvdwOmocNF30xT1WVlmibYg==
+slate-hotkeys@^0.1.2:
+  version "0.1.4"
+  resolved "https://registry.yarnpkg.com/slate-hotkeys/-/slate-hotkeys-0.1.4.tgz#5b10b2a178affc60827f9284d4c0a5d7e5041ffe"
+  dependencies:
+    is-hotkey "^0.1.1"
+    slate-dev-environment "^0.1.4"
 
-slate-prop-types@^0.5.41:
+slate-plain-serializer@0.5.41, slate-plain-serializer@^0.5.17:
   version "0.5.41"
-  resolved "https://registry.yarnpkg.com/slate-prop-types/-/slate-prop-types-0.5.41.tgz#42031881e2fef4fa978a96b9aad84b093b4a5219"
-  integrity sha512-fLcXlugO9btF5b/by+dA+n8fn2mET75VGWltqFNxGdl6ncyBtrGspWA7mLVRFSqQWOS/Ig4A3URCRumOBBCUfQ==
+  resolved "https://registry.yarnpkg.com/slate-plain-serializer/-/slate-plain-serializer-0.5.41.tgz#dc2d219602c2cb8dc710ac660e108f3b3cc4dc80"
+  dependencies:
+    slate-dev-logger "^0.1.43"
+
+slate-prism@0.5.0:
+  version "0.5.0"
+  resolved "https://registry.yarnpkg.com/slate-prism/-/slate-prism-0.5.0.tgz#009eb74fea38ad76c64db67def7ea0884917adec"
+  dependencies:
+    prismjs "^1.13.0"
 
-slate-react-placeholder@^0.2.8:
-  version "0.2.8"
-  resolved "https://registry.yarnpkg.com/slate-react-placeholder/-/slate-react-placeholder-0.2.8.tgz#973ac47c9a518a1418e89b6021b0f6120c07ce6f"
-  integrity sha512-CZZSg5usE2ZY/AYg06NVcL9Wia6hD/Mg0w4D4e9rPh6hkkFJg8LZXYMRz+6Q4v1dqHmzRsZ2Ixa0jRuiKXsMaQ==
+slate-prop-types@^0.4.34:
+  version "0.4.67"
+  resolved "https://registry.yarnpkg.com/slate-prop-types/-/slate-prop-types-0.4.67.tgz#c6aa74195466546a44fcb85d1c7b15fefe36ce6b"
 
-slate@0.47.8:
-  version "0.47.8"
-  resolved "https://registry.yarnpkg.com/slate/-/slate-0.47.8.tgz#1e987b74d8216d44ec56154f0e6d3c722ce21e6e"
-  integrity sha512-/Jt0eq4P40qZvtzeKIvNb+1N97zVICulGQgQoMDH0TI8h8B+5kqa1YeckRdRnuvfYJm3J/9lWn2V3J1PrF+hag==
+slate-react@0.12.11:
+  version "0.12.11"
+  resolved "https://registry.yarnpkg.com/slate-react/-/slate-react-0.12.11.tgz#6d83e604634704757690a57dbd6aab282a964ad3"
+  dependencies:
+    debug "^3.1.0"
+    get-window "^1.1.1"
+    is-window "^1.0.2"
+    keycode "^2.1.2"
+    lodash "^4.1.1"
+    prop-types "^15.5.8"
+    react-immutable-proptypes "^2.1.0"
+    react-portal "^3.1.0"
+    selection-is-backward "^1.0.0"
+    slate-base64-serializer "^0.2.36"
+    slate-dev-environment "^0.1.2"
+    slate-dev-logger "^0.1.39"
+    slate-hotkeys "^0.1.2"
+    slate-plain-serializer "^0.5.17"
+    slate-prop-types "^0.4.34"
+
+slate-schema-violations@^0.1.12:
+  version "0.1.39"
+  resolved "https://registry.yarnpkg.com/slate-schema-violations/-/slate-schema-violations-0.1.39.tgz#854ab5624136419cef4c803b1823acabe11f1c15"
+
+slate@0.33.8:
+  version "0.33.8"
+  resolved "https://registry.yarnpkg.com/slate/-/slate-0.33.8.tgz#c2cd9906c446d010b15e9e28f6d1a01792c7a113"
   dependencies:
     debug "^3.1.0"
     direction "^0.1.5"
     esrever "^0.2.0"
+    is-empty "^1.0.0"
     is-plain-object "^2.0.4"
     lodash "^4.17.4"
-    tiny-invariant "^1.0.1"
-    tiny-warning "^0.0.3"
+    slate-dev-logger "^0.1.39"
+    slate-schema-violations "^0.1.12"
     type-of "^2.0.1"
 
 slice-ansi@0.0.4:
@@ -17462,20 +17466,14 @@ tiny-emitter@^2.0.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423"
 
-tiny-invariant@^1.0.1, tiny-invariant@^1.0.2:
-  version "1.0.6"
-  resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.0.6.tgz#b3f9b38835e36a41c843a3b0907a5a7b3755de73"
-  integrity sha512-FOyLWWVjG+aC0UqG76V53yAWdXfH8bO6FNmyZOuUrzDzK8DI3/JRY25UD7+g49JWM1LXwymsKERB+DzI0dTEQA==
+tiny-invariant@^1.0.2:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.0.4.tgz#346b5415fd93cb696b0c4e8a96697ff590f92463"
 
 tiny-relative-date@^1.3.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/tiny-relative-date/-/tiny-relative-date-1.3.0.tgz#fa08aad501ed730f31cc043181d995c39a935e07"
 
-tiny-warning@^0.0.3:
-  version "0.0.3"
-  resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-0.0.3.tgz#1807eb4c5f81784a6354d58ea1d5024f18c6c81f"
-  integrity sha512-r0SSA5Y5IWERF9Xh++tFPx0jITBgGggOsRLDWWew6YRw/C2dr4uNO1fw1vanrBmHsICmPyMLNBZboTlxUmUuaA==
-
 tiny-warning@^1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.2.tgz#1dfae771ee1a04396bdfde27a3adcebc6b648b28"
@@ -17784,7 +17782,6 @@ type-name@^2.0.1:
 type-of@^2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/type-of/-/type-of-2.0.1.tgz#e72a1741896568e9f628378d816d6912f7f23972"
-  integrity sha1-5yoXQYllaOn2KDeNgW1pEvfyOXI=
 
 typed-styles@^0.0.7:
   version "0.0.7"
@@ -18631,6 +18628,7 @@ wrap-ansi@^5.1.0:
 wrappy@1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+  integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
 
 write-file-atomic@2.4.1:
   version "2.4.1"