Browse Source

[editor] Fix drag and drop from assist to the AceEditor component in editor v2

Johan Ahlen 4 năm trước cách đây
mục cha
commit
4dc127de31

+ 74 - 13
desktop/core/src/desktop/js/apps/editor/components/aceEditor/AceEditor.test.ts

@@ -16,31 +16,92 @@
 
 import { shallowMount } from '@vue/test-utils';
 import { CancellablePromise } from 'api/cancellablePromise';
+import Executor from 'apps/editor/execution/executor';
 import dataCatalog from 'catalog/dataCatalog';
+import { Ace } from 'ext/ace';
+import { INSERT_AT_CURSOR_EVENT } from 'ko/bindings/ace/ko.aceEditor';
+import huePubSub from 'utils/huePubSub';
+import { nextTick } from 'vue';
 import AceEditor from './AceEditor.vue';
 
 describe('AceEditor.vue', () => {
+  const mockExecutor = ({
+    connector: ko.observable({
+      dialect: 'foo',
+      id: 'foo'
+    }),
+    namespace: ko.observable({
+      id: 'foo'
+    }),
+    compute: ko.observable({
+      id: 'foo'
+    })
+  } as unknown) as Executor;
+
   it('should render', () => {
     spyOn(dataCatalog, 'getChildren').and.returnValue(CancellablePromise.resolve([]));
 
     const wrapper = shallowMount(AceEditor, {
-      propsData: {
+      props: {
         value: 'some query',
         id: 'some-id',
-        executor: {
-          connector: ko.observable({
-            dialect: 'foo',
-            id: 'foo'
-          }),
-          namespace: ko.observable({
-            id: 'foo'
-          }),
-          compute: ko.observable({
-            id: 'foo'
-          })
-        }
+        executor: mockExecutor
       }
     });
     expect(wrapper.element).toMatchSnapshot();
   });
+
+  it('should handle drag and drop pubsub event targeting this editor', async () => {
+    spyOn(dataCatalog, 'getChildren').and.returnValue(CancellablePromise.resolve([]));
+
+    const wrapper = shallowMount(AceEditor, {
+      props: {
+        value: '',
+        id: 'some-id',
+        executor: mockExecutor
+      }
+    });
+
+    await nextTick();
+
+    expect(wrapper.emitted()['ace-created']).toBeTruthy();
+
+    const editor = (wrapper.emitted()['ace-created'][0] as Ace.Editor[])[0];
+
+    const draggedText = 'Some dropped text';
+    huePubSub.publish(INSERT_AT_CURSOR_EVENT, {
+      text: draggedText,
+      targetEditor: editor,
+      cursorEndAdjust: 0
+    });
+
+    expect(editor.getValue()).toEqual(draggedText);
+  });
+
+  it('should not handle drag and drop pubsub event targeting another editor', async () => {
+    spyOn(dataCatalog, 'getChildren').and.returnValue(CancellablePromise.resolve([]));
+
+    const wrapper = shallowMount(AceEditor, {
+      props: {
+        value: '',
+        id: 'some-id',
+        executor: mockExecutor
+      }
+    });
+
+    await nextTick();
+
+    expect(wrapper.emitted()['ace-created']).toBeTruthy();
+
+    const editor = (wrapper.emitted()['ace-created'][0] as Ace.Editor[])[0];
+
+    const draggedText = 'Some dropped text';
+    huePubSub.publish(INSERT_AT_CURSOR_EVENT, {
+      text: draggedText,
+      targetEditor: {}, // Other instance
+      cursorEndAdjust: 0
+    });
+
+    expect(editor.getValue()).not.toEqual(draggedText);
+  });
 });

+ 410 - 429
desktop/core/src/desktop/js/apps/editor/components/aceEditor/AceEditor.vue

@@ -31,7 +31,8 @@
 </template>
 
 <script lang="ts">
-  import { defineComponent, PropType } from 'vue';
+  import { ActiveStatementChangedEventDetails } from 'apps/editor/components/aceEditor/types';
+  import { defineComponent, onMounted, PropType, ref, toRefs } from 'vue';
 
   import ace, { getAceMode } from 'ext/aceHelper';
   import { Ace } from 'ext/ace';
@@ -111,334 +112,58 @@
       'ace-created',
       'cursor-changed'
     ],
-    setup() {
+    setup(props, { emit }) {
+      const {
+        id,
+        sqlReferenceProvider,
+        executor,
+        initialCursorPosition,
+        sqlParserProvider,
+        initialValue,
+        aceOptions
+      } = toRefs(props);
       const subTracker = new SubscriptionTracker();
-      return { subTracker };
-    },
-    data() {
-      return {
-        editor: null as Ace.Editor | null,
-        autocompleteParser: null as AutocompleteParser | null,
-        aceLocationHandler: null as AceLocationHandler | null,
-        lastFocusedEditor: false
-      };
-    },
-    mounted(): void {
-      const editorElement = <HTMLElement>this.$refs['editorElement'];
-      if (!editorElement) {
-        return;
-      }
+      const editorElement = ref<HTMLElement | null>(null);
 
-      if (this.sqlParserProvider) {
-        this.sqlParserProvider
-          .getAutocompleteParser(this.executor.connector().dialect || 'generic')
-          .then(autocompleteParser => {
-            this.autocompleteParser = autocompleteParser;
-          });
-      }
-
-      editorElement.textContent = this.initialValue;
-      const editor = <Ace.Editor>ace.edit(editorElement);
-
-      this.configureEditorOptions(editor);
-      this.addCustomAceConfigOptions(editor);
-
-      editor.$blockScrolling = Infinity;
-
-      this.aceLocationHandler = new AceLocationHandler({
-        editor: editor,
-        editorId: this.id,
-        executor: this.executor,
-        sqlReferenceProvider: this.sqlReferenceProvider
-      });
-      this.subTracker.addDisposable(this.aceLocationHandler);
+      const editorRef = ref<Ace.Editor | null>(null);
+      let lastFocusedEditor = false;
+      const autocompleteParser = ref<AutocompleteParser | null>(null);
 
-      this.subTracker.subscribe(
-        ACTIVE_STATEMENT_CHANGED_EVENT,
-        (details: ActiveStatementChangedEventDetails) => {
-          if (this.id === details.id) {
-            this.$emit('active-statement-changed', details);
-          }
-        }
-      );
-
-      const aceGutterHandler = new AceGutterHandler({
-        editor: editor,
-        editorId: this.id,
-        executor: this.executor
-      });
-      this.subTracker.addDisposable(aceGutterHandler);
-
-      editor.session.setMode(getAceMode(this.executor.connector().dialect));
-
-      if ((<hueWindow>window).ENABLE_SQL_SYNTAX_CHECK && window.Worker) {
-        let errorHighlightingEnabled = getFromLocalStorage(
-          'hue.ace.errorHighlightingEnabled',
+      const configureEditorOptions = (editor: Ace.Editor): void => {
+        const enableBasicAutocompletion = getFromLocalStorage(
+          'hue.ace.enableBasicAutocompletion',
           true
         );
 
-        if (errorHighlightingEnabled) {
-          this.aceLocationHandler.attachSqlSyntaxWorker();
-        }
+        const enableLiveAutocompletion =
+          enableBasicAutocompletion &&
+          getFromLocalStorage('hue.ace.enableLiveAutocompletion', true);
 
-        editor.customMenuOptions.setErrorHighlighting = (enabled: boolean) => {
-          errorHighlightingEnabled = enabled;
-          setInLocalStorage('hue.ace.errorHighlightingEnabled', enabled);
-          if (this.aceLocationHandler) {
-            if (enabled) {
-              this.aceLocationHandler.attachSqlSyntaxWorker();
-            } else {
-              this.aceLocationHandler.detachSqlSyntaxWorker();
-            }
-          }
-        };
-        editor.customMenuOptions.getErrorHighlighting = () => errorHighlightingEnabled;
-        editor.customMenuOptions.setClearIgnoredSyntaxChecks = () => {
-          setInLocalStorage('hue.syntax.checker.suppressedRules', {});
-          const el = document.getElementById('setClearIgnoredSyntaxChecks');
-          if (!el || !el.parentNode) {
-            return;
-          }
-          el.style.display = 'none';
-          const doneElem = document.createElement('div');
-          doneElem.style.marginTop = '5px';
-          doneElem.style.float = 'right';
-          doneElem.innerText = 'done';
-          el.insertAdjacentElement('beforebegin', doneElem);
+        const editorOptions: Ace.Options = {
+          enableBasicAutocompletion,
+          enableLiveAutocompletion,
+          fontSize: getFromLocalStorage(
+            'hue.ace.fontSize',
+            navigator.platform && navigator.platform.toLowerCase().indexOf('linux') > -1
+              ? '14px'
+              : '12px'
+          ),
+          enableSnippets: true,
+          showGutter: true,
+          showLineNumbers: true,
+          showPrintMargin: false,
+          scrollPastEnd: 0.1,
+          minLines: 3,
+          maxLines: 25,
+          tabSize: 2,
+          useSoftTabs: true,
+          ...aceOptions.value
         };
-        editor.customMenuOptions.getClearIgnoredSyntaxChecks = () => false;
-      }
 
-      const AceAutocomplete = ace.require('ace/autocomplete').Autocomplete;
-
-      if (!editor.completer) {
-        editor.completer = new AceAutocomplete();
-      }
-      editor.completer.exactMatch = !this.isSqlDialect();
-
-      const langTools = ace.require('ace/ext/language_tools');
-      langTools.textCompleter.setSqlMode(this.isSqlDialect());
-
-      if (editor.completers) {
-        editor.completers.length = 0;
-        if (this.isSqlDialect()) {
-          editor.useHueAutocompleter = true;
-        } else {
-          editor.completers.push(langTools.snippetCompleter);
-          editor.completers.push(langTools.textCompleter);
-          editor.completers.push(langTools.keyWordCompleter);
-        }
-      }
-
-      const onFocus = (): void => {
-        huePubSub.publish('ace.editor.focused', editor);
-
-        // TODO: Figure out why this is needed
-        if (editor.session.$backMarkers) {
-          for (const marker in editor.session.$backMarkers) {
-            if (editor.session.$backMarkers[marker].clazz === 'highlighted') {
-              editor.session.removeMarker(editor.session.$backMarkers[marker].id);
-            }
-          }
-        }
-      };
-
-      const onPaste = (e: { text: string }): void => {
-        e.text = removeUnicodes(e.text);
-      };
-
-      if ((<hueWindow>window).ENABLE_PREDICT) {
-        attachPredictTypeahead(editor, this.executor.connector());
-      }
-
-      let placeholderVisible = false;
-      const placeholderElement = this.createPlaceholderElement();
-      const onInput = () => {
-        if (!placeholderVisible && !editor.getValue().length) {
-          editor.renderer.scroller.append(placeholderElement);
-          placeholderVisible = true;
-        } else if (placeholderVisible) {
-          placeholderElement.remove();
-          placeholderVisible = false;
-        }
-      };
-
-      onInput();
-
-      const onMouseDown = (e: { domEvent: MouseEvent; $pos?: Ace.Position }): void => {
-        if (e.domEvent.button === 1) {
-          // middle click
-          const position = e.$pos;
-          if (!position) {
-            return;
-          }
-          const tempText = editor.getSelectedText();
-          editor.session.insert(position, tempText);
-          defer(() => {
-            editor.moveCursorTo(position.row, position.column + tempText.length);
-          });
-        }
-      };
-
-      const boundTriggerChange = this.triggerChange.bind(this);
-      editor.on('change', boundTriggerChange);
-      editor.on('blur', boundTriggerChange);
-      editor.on('focus', onFocus);
-      editor.on('paste', onPaste);
-      editor.on('input', onInput);
-      editor.on('mousedown', onMouseDown);
-
-      this.subTracker.addDisposable({
-        dispose: () => {
-          editor.off('change', boundTriggerChange);
-          editor.off('blur', boundTriggerChange);
-          editor.off('focus', onFocus);
-          editor.off('paster', onPaste);
-          editor.off('input', onInput);
-          editor.off('mousedown', onMouseDown);
-        }
-      });
-
-      const resizeAce = () => {
-        defer(() => {
-          try {
-            editor.resize(true);
-          } catch (e) {
-            // Can happen when the editor hasn't been initialized
-          }
-        });
+        editor.setOptions(editorOptions);
       };
 
-      this.subTracker.subscribe('ace.editor.focused', (editor: Ace.Editor): void => {
-        this.lastFocusedEditor = editor === this.editor;
-      });
-
-      this.subTracker.subscribe('assist.set.manual.visibility', resizeAce);
-      this.subTracker.subscribe('split.panel.resized', resizeAce);
-
-      this.subTracker.subscribe(
-        'ace.replace',
-        (data: { text: string; location: ParsedLocation }): void => {
-          const Range = ace.require('ace/range').Range;
-          const range = new Range(
-            data.location.first_line - 1,
-            data.location.first_column - 1,
-            data.location.last_line - 1,
-            data.location.last_column - 1
-          );
-          editor.getSession().getDocument().replace(range, data.text);
-        }
-      );
-
-      if (this.initialCursorPosition) {
-        editor.moveCursorToPosition(this.initialCursorPosition);
-        editor.renderer.scrollCursorIntoView();
-      }
-
-      this.subTracker.subscribe(
-        CURSOR_POSITION_CHANGED_EVENT,
-        (event: { editorId: string; position: Ace.Position }) => {
-          if (event.editorId === this.id) {
-            this.$emit('cursor-changed', event.position);
-          }
-        }
-      );
-
-      this.editor = editor;
-      this.registerEditorCommands();
-      this.addInsertSubscribers();
-      this.$emit('ace-created', editor);
-    },
-
-    unmounted(): void {
-      this.subTracker.dispose();
-    },
-
-    methods: {
-      I18n,
-
-      isSqlDialect(): boolean {
-        return (<EditorInterpreter>this.executor.connector()).is_sql;
-      },
-
-      createPlaceholderElement(): HTMLElement {
-        const element = document.createElement('div');
-        element.innerText = I18n('Example: SELECT * FROM tablename, or press CTRL + space');
-        element.style.marginLeft = '6px';
-        element.classList.add('ace_invisible');
-        element.classList.add('ace_emptyMessage');
-        return element;
-      },
-
-      cursorAtStartOfStatement(): boolean {
-        return (
-          !!this.editor &&
-          (/^\s*$/.test(this.editor.getValue()) ||
-            /^.*;\s*$/.test(this.editor.getTextBeforeCursor()))
-        );
-      },
-
-      addInsertSubscribers(): void {
-        this.subTracker.subscribe(
-          INSERT_AT_CURSOR_EVENT,
-          (details: { text: string; targetEditor: Ace.Editor; cursorEndAdjust?: number }): void => {
-            if (details.targetEditor === this.editor || this.lastFocusedEditor) {
-              this.insertSqlAtCursor(details.text, details.cursorEndAdjust);
-            }
-          }
-        );
-
-        this.subTracker.subscribe(
-          'editor.insert.table.at.cursor',
-          (details: { name: string; database: string }) => {
-            if (!this.lastFocusedEditor) {
-              return;
-            }
-            const qualifiedName =
-              this.executor.database() === details.database
-                ? details.name
-                : `${details.database}.${details.name}`;
-            if (this.cursorAtStartOfStatement()) {
-              this.insertSqlAtCursor(`SELECT * FROM ${qualifiedName} LIMIT 100;`, -1);
-            } else {
-              this.insertSqlAtCursor(`${qualifiedName} `);
-            }
-          }
-        );
-
-        this.subTracker.subscribe(
-          'editor.insert.column.at.cursor',
-          (details: { name: string; table: string; database: string }): void => {
-            if (!this.lastFocusedEditor) {
-              return;
-            }
-            if (this.cursorAtStartOfStatement()) {
-              const qualifiedFromName =
-                this.executor.database() === details.database
-                  ? details.table
-                  : details.database + '.' + details.table;
-              this.insertSqlAtCursor(
-                `SELECT ${details.name} FROM ${qualifiedFromName} LIMIT 100;`,
-                -1
-              );
-            }
-          }
-        );
-
-        this.subTracker.subscribe(
-          'sample.error.insert.click',
-          (popoverEntry: { identifierChain: IdentifierChainEntry[] }): void => {
-            if (!this.lastFocusedEditor || !popoverEntry.identifierChain.length) {
-              return;
-            }
-            const table =
-              popoverEntry.identifierChain[popoverEntry.identifierChain.length - 1].name;
-            this.insertSqlAtCursor(`SELECT * FROM ${table} LIMIT 100;`, -1);
-          }
-        );
-      },
-
-      addCustomAceConfigOptions(editor: Ace.Editor): void {
+      const addCustomAceConfigOptions = (editor: Ace.Editor): void => {
         let darkThemeEnabled = getFromLocalStorage('ace.dark.theme.enabled', false);
 
         editor.setTheme(darkThemeEnabled ? 'ace/theme/hue_dark' : 'ace/theme/hue');
@@ -497,162 +222,418 @@
             return size;
           }
         };
-      },
-
-      configureEditorOptions(editor: Ace.Editor): void {
-        const enableBasicAutocompletion = getFromLocalStorage(
-          'hue.ace.enableBasicAutocompletion',
-          true
-        );
-
-        const enableLiveAutocompletion =
-          enableBasicAutocompletion &&
-          getFromLocalStorage('hue.ace.enableLiveAutocompletion', true);
-
-        const editorOptions: Ace.Options = {
-          enableBasicAutocompletion,
-          enableLiveAutocompletion,
-          fontSize: getFromLocalStorage(
-            'hue.ace.fontSize',
-            navigator.platform && navigator.platform.toLowerCase().indexOf('linux') > -1
-              ? '14px'
-              : '12px'
-          ),
-          enableSnippets: true,
-          showGutter: true,
-          showLineNumbers: true,
-          showPrintMargin: false,
-          scrollPastEnd: 0.1,
-          minLines: 3,
-          maxLines: 25,
-          tabSize: 2,
-          useSoftTabs: true,
-          ...this.aceOptions
-        };
-
-        editor.setOptions(editorOptions);
-      },
-
-      insertSqlAtCursor(text: string, cursorEndAdjust?: number): void {
-        if (!this.editor) {
-          return;
-        }
-
-        const before = this.editor.getTextBeforeCursor();
-
-        const textToInsert = /\S+$/.test(before) ? ' ' + text : text;
-        this.editor.session.insert(this.editor.getCursorPosition(), textToInsert);
-        if (cursorEndAdjust) {
-          const positionAfterInsert = this.editor.getCursorPosition();
-          this.editor.moveCursorToPosition({
-            row: positionAfterInsert.row,
-            column: positionAfterInsert.column + cursorEndAdjust
-          });
-        }
-        this.editor.clearSelection();
-        this.editor.focus();
-      },
-
-      triggerChange(): void {
-        if (this.editor) {
-          this.$emit('value-changed', removeUnicodes(this.editor.getValue()));
-        }
-      },
-
-      registerEditorCommands(): void {
-        if (!this.editor) {
-          return;
-        }
+      };
 
-        this.editor.commands.addCommand({
+      const registerEditorCommands = (
+        editor: Ace.Editor,
+        aceLocationHandler: AceLocationHandler,
+        triggerChange: () => void
+      ): void => {
+        editor.commands.addCommand({
           name: 'execute',
           bindKey: { win: 'Ctrl-Enter', mac: 'Command-Enter|Ctrl-Enter' },
           exec: async () => {
-            if (this.aceLocationHandler) {
-              this.aceLocationHandler.refreshStatementLocations();
-            }
-            if (this.editor && this.executor.activeExecutable) {
-              this.triggerChange();
-              await this.executor.activeExecutable.reset();
-              await this.executor.activeExecutable.execute();
+            aceLocationHandler.refreshStatementLocations();
+            if (editor && executor.value.activeExecutable) {
+              triggerChange();
+              await executor.value.activeExecutable.reset();
+              await executor.value.activeExecutable.execute();
             }
           }
         });
 
-        this.editor.commands.addCommand({
+        editor.commands.addCommand({
           name: 'switchTheme',
           bindKey: { win: 'Ctrl-Alt-t', mac: 'Command-Alt-t' },
           exec: () => {
             if (
-              this.editor &&
-              this.editor.customMenuOptions &&
-              this.editor.customMenuOptions.getEnableDarkTheme &&
-              this.editor.customMenuOptions.setEnableDarkTheme
+              editor.customMenuOptions &&
+              editor.customMenuOptions.getEnableDarkTheme &&
+              editor.customMenuOptions.setEnableDarkTheme
             ) {
-              const enabled = this.editor.customMenuOptions.getEnableDarkTheme();
-              this.editor.customMenuOptions.setEnableDarkTheme(!enabled);
+              const enabled = editor.customMenuOptions.getEnableDarkTheme();
+              editor.customMenuOptions.setEnableDarkTheme(!enabled);
             }
           }
         });
 
-        this.editor.commands.addCommand({
+        editor.commands.addCommand({
           name: 'new',
           bindKey: { win: 'Ctrl-e', mac: 'Command-e' },
           exec: () => {
-            this.$emit('create-new-doc');
+            emit('create-new-doc');
           }
         });
 
-        this.editor.commands.addCommand({
+        editor.commands.addCommand({
           name: 'save',
           bindKey: { win: 'Ctrl-s', mac: 'Command-s|Ctrl-s' },
           exec: () => {
-            this.$emit('save-doc');
+            emit('save-doc');
           }
         });
 
-        this.editor.commands.addCommand({
+        editor.commands.addCommand({
           name: 'togglePresentationMode',
           bindKey: { win: 'Ctrl-Shift-p', mac: 'Ctrl-Shift-p|Command-Shift-p' },
           exec: () => {
-            this.$emit('toggle-presentation-mode');
+            emit('toggle-presentation-mode');
           }
         });
 
-        this.editor.commands.addCommand({
+        editor.commands.addCommand({
           name: 'format',
           bindKey: {
             win: 'Ctrl-i|Ctrl-Shift-f|Ctrl-Alt-l',
             mac: 'Command-i|Ctrl-i|Ctrl-Shift-f|Command-Shift-f|Ctrl-Shift-l|Cmd-Shift-l'
           },
           exec: async () => {
-            if (this.editor) {
-              this.editor.setReadOnly(true);
-              try {
-                if (this.editor.getSelectedText()) {
-                  const selectionRange = this.editor.getSelectionRange();
-                  const formatted = await formatSql({
-                    statements: this.editor.getSelectedText(),
-                    silenceErrors: true
-                  });
-                  this.editor.getSession().replace(selectionRange, formatted);
-                } else {
-                  const formatted = await formatSql({
-                    statements: this.editor.getValue(),
-                    silenceErrors: true
-                  });
-                  this.editor.setValue(formatted, 1);
-                }
-                this.triggerChange();
-              } catch (e) {}
-              this.editor.setReadOnly(false);
-            }
+            editor.setReadOnly(true);
+            try {
+              if (editor.getSelectedText()) {
+                const selectionRange = editor.getSelectionRange();
+                const formatted = await formatSql({
+                  statements: editor.getSelectedText(),
+                  silenceErrors: true
+                });
+                editor.getSession().replace(selectionRange, formatted);
+              } else {
+                const formatted = await formatSql({
+                  statements: editor.getValue(),
+                  silenceErrors: true
+                });
+                editor.setValue(formatted, 1);
+              }
+              triggerChange();
+            } catch (e) {}
+            editor.setReadOnly(false);
           }
         });
 
-        this.editor.commands.bindKey('Ctrl-P', 'golineup');
-        this.editor.commands.bindKey({ win: 'Ctrl-j', mac: 'Command-j|Ctrl-j' }, 'gotoline');
+        editor.commands.bindKey('Ctrl-P', 'golineup');
+        editor.commands.bindKey({ win: 'Ctrl-j', mac: 'Command-j|Ctrl-j' }, 'gotoline');
+      };
+
+      const addInsertSubscribers = (editor: Ace.Editor): void => {
+        const cursorAtStartOfStatement = (): boolean => {
+          return /^\s*$/.test(editor.getValue()) || /^.*;\s*$/.test(editor.getTextBeforeCursor());
+        };
+
+        const insertSqlAtCursor = (text: string, cursorEndAdjust?: number): void => {
+          const before = editor.getTextBeforeCursor();
+
+          const textToInsert = /\S+$/.test(before) ? ' ' + text : text;
+          editor.session.insert(editor.getCursorPosition(), textToInsert);
+          if (cursorEndAdjust) {
+            const positionAfterInsert = editor.getCursorPosition();
+            editor.moveCursorToPosition({
+              row: positionAfterInsert.row,
+              column: positionAfterInsert.column + cursorEndAdjust
+            });
+          }
+          editor.clearSelection();
+          editor.focus();
+        };
+
+        subTracker.subscribe(
+          INSERT_AT_CURSOR_EVENT,
+          (details: { text: string; targetEditor: Ace.Editor; cursorEndAdjust?: number }): void => {
+            if (details.targetEditor === editor || lastFocusedEditor) {
+              insertSqlAtCursor(details.text, details.cursorEndAdjust);
+            }
+          }
+        );
+
+        subTracker.subscribe(
+          'editor.insert.table.at.cursor',
+          (details: { name: string; database: string }) => {
+            if (!lastFocusedEditor) {
+              return;
+            }
+            const qualifiedName =
+              executor.value.database() === details.database
+                ? details.name
+                : `${details.database}.${details.name}`;
+            if (cursorAtStartOfStatement()) {
+              insertSqlAtCursor(`SELECT * FROM ${qualifiedName} LIMIT 100;`, -1);
+            } else {
+              insertSqlAtCursor(`${qualifiedName} `);
+            }
+          }
+        );
+
+        subTracker.subscribe(
+          'editor.insert.column.at.cursor',
+          (details: { name: string; table: string; database: string }): void => {
+            if (!lastFocusedEditor) {
+              return;
+            }
+            if (cursorAtStartOfStatement()) {
+              const qualifiedFromName =
+                executor.value.database() === details.database
+                  ? details.table
+                  : details.database + '.' + details.table;
+              insertSqlAtCursor(`SELECT ${details.name} FROM ${qualifiedFromName} LIMIT 100;`, -1);
+            }
+          }
+        );
+
+        subTracker.subscribe(
+          'sample.error.insert.click',
+          (popoverEntry: { identifierChain: IdentifierChainEntry[] }): void => {
+            if (!lastFocusedEditor || !popoverEntry.identifierChain.length) {
+              return;
+            }
+            const table =
+              popoverEntry.identifierChain[popoverEntry.identifierChain.length - 1].name;
+            insertSqlAtCursor(`SELECT * FROM ${table} LIMIT 100;`, -1);
+          }
+        );
+      };
+
+      if (sqlParserProvider.value) {
+        sqlParserProvider.value
+          .getAutocompleteParser(executor.value.connector().dialect || 'generic')
+          .then(parser => {
+            autocompleteParser.value = parser;
+          });
       }
+
+      onMounted(() => {
+        const element = editorElement.value;
+        if (!element) {
+          return;
+        }
+
+        element.textContent = initialValue.value;
+
+        const editor = <Ace.Editor>ace.edit(element);
+
+        configureEditorOptions(editor);
+        addCustomAceConfigOptions(editor);
+
+        const aceLocationHandler = new AceLocationHandler({
+          editor,
+          editorId: id.value,
+          executor: executor.value as Executor,
+          sqlReferenceProvider: sqlReferenceProvider.value
+        });
+        subTracker.addDisposable(aceLocationHandler);
+
+        editor.$blockScrolling = Infinity;
+
+        const aceGutterHandler = new AceGutterHandler({
+          editor,
+          editorId: id.value,
+          executor: executor.value as Executor
+        });
+        subTracker.addDisposable(aceGutterHandler);
+
+        editor.session.setMode(getAceMode(executor.value.connector().dialect));
+
+        if ((<hueWindow>window).ENABLE_SQL_SYNTAX_CHECK && window.Worker) {
+          let errorHighlightingEnabled = getFromLocalStorage(
+            'hue.ace.errorHighlightingEnabled',
+            true
+          );
+
+          if (errorHighlightingEnabled) {
+            aceLocationHandler.attachSqlSyntaxWorker();
+          }
+
+          editor.customMenuOptions.setErrorHighlighting = (enabled: boolean) => {
+            errorHighlightingEnabled = enabled;
+            setInLocalStorage('hue.ace.errorHighlightingEnabled', enabled);
+            if (enabled) {
+              aceLocationHandler.attachSqlSyntaxWorker();
+            } else {
+              aceLocationHandler.detachSqlSyntaxWorker();
+            }
+          };
+          editor.customMenuOptions.getErrorHighlighting = () => errorHighlightingEnabled;
+          editor.customMenuOptions.setClearIgnoredSyntaxChecks = () => {
+            setInLocalStorage('hue.syntax.checker.suppressedRules', {});
+            const el = document.getElementById('setClearIgnoredSyntaxChecks');
+            if (!el || !el.parentNode) {
+              return;
+            }
+            el.style.display = 'none';
+            const doneElem = document.createElement('div');
+            doneElem.style.marginTop = '5px';
+            doneElem.style.float = 'right';
+            doneElem.innerText = 'done';
+            el.insertAdjacentElement('beforebegin', doneElem);
+          };
+          editor.customMenuOptions.getClearIgnoredSyntaxChecks = () => false;
+
+          const AceAutocomplete = ace.require('ace/autocomplete').Autocomplete;
+
+          if (!editor.completer) {
+            editor.completer = new AceAutocomplete();
+          }
+
+          const isSqlDialect = (<EditorInterpreter>executor.value.connector()).is_sql;
+
+          editor.completer.exactMatch = !isSqlDialect;
+
+          const langTools = ace.require('ace/ext/language_tools');
+          langTools.textCompleter.setSqlMode(isSqlDialect);
+
+          if (editor.completers) {
+            editor.completers.length = 0;
+            if (isSqlDialect) {
+              editor.useHueAutocompleter = true;
+            } else {
+              editor.completers.push(langTools.snippetCompleter);
+              editor.completers.push(langTools.textCompleter);
+              editor.completers.push(langTools.keyWordCompleter);
+            }
+          }
+        }
+
+        const onFocus = (): void => {
+          huePubSub.publish('ace.editor.focused', editor);
+
+          // TODO: Figure out why this is needed
+          if (editor.session.$backMarkers) {
+            for (const marker in editor.session.$backMarkers) {
+              if (editor.session.$backMarkers[marker].clazz === 'highlighted') {
+                editor.session.removeMarker(editor.session.$backMarkers[marker].id);
+              }
+            }
+          }
+        };
+
+        const onPaste = (e: { text: string }): void => {
+          e.text = removeUnicodes(e.text);
+        };
+
+        if ((<hueWindow>window).ENABLE_PREDICT) {
+          attachPredictTypeahead(editor, executor.value.connector());
+        }
+
+        let placeholderVisible = false;
+
+        const createPlaceholderElement = (): HTMLElement => {
+          const element = document.createElement('div');
+          element.innerText = I18n('Example: SELECT * FROM tablename, or press CTRL + space');
+          element.style.marginLeft = '6px';
+          element.classList.add('ace_invisible');
+          element.classList.add('ace_emptyMessage');
+          return element;
+        };
+
+        const placeholderElement = createPlaceholderElement();
+        const onInput = () => {
+          if (!placeholderVisible && !editor.getValue().length) {
+            editor.renderer.scroller.append(placeholderElement);
+            placeholderVisible = true;
+          } else if (placeholderVisible) {
+            placeholderElement.remove();
+            placeholderVisible = false;
+          }
+        };
+
+        onInput();
+
+        const onMouseDown = (e: { domEvent: MouseEvent; $pos?: Ace.Position }): void => {
+          if (e.domEvent.button === 1) {
+            // middle click
+            const position = e.$pos;
+            if (!position) {
+              return;
+            }
+            const tempText = editor.getSelectedText();
+            editor.session.insert(position, tempText);
+            defer(() => {
+              editor.moveCursorTo(position.row, position.column + tempText.length);
+            });
+          }
+        };
+
+        const triggerChange = (): void => {
+          emit('value-changed', removeUnicodes(editor.getValue()));
+        };
+
+        editor.on('change', triggerChange);
+        editor.on('blur', triggerChange);
+        editor.on('focus', onFocus);
+        editor.on('paste', onPaste);
+        editor.on('input', onInput);
+        editor.on('mousedown', onMouseDown);
+
+        subTracker.addDisposable({
+          dispose: () => {
+            editor.off('change', triggerChange);
+            editor.off('blur', triggerChange);
+            editor.off('focus', onFocus);
+            editor.off('paster', onPaste);
+            editor.off('input', onInput);
+            editor.off('mousedown', onMouseDown);
+          }
+        });
+
+        const resizeAce = () => {
+          defer(() => {
+            try {
+              editor.resize(true);
+            } catch (e) {
+              // Can happen when the editor hasn't been initialized
+            }
+          });
+        };
+
+        subTracker.subscribe('ace.editor.focused', (focusedEditor: Ace.Editor): void => {
+          lastFocusedEditor = editor === focusedEditor;
+        });
+
+        subTracker.subscribe('assist.set.manual.visibility', resizeAce);
+        subTracker.subscribe('split.panel.resized', resizeAce);
+
+        subTracker.subscribe(
+          'ace.replace',
+          (data: { text: string; location: ParsedLocation }): void => {
+            const Range = ace.require('ace/range').Range;
+            const range = new Range(
+              data.location.first_line - 1,
+              data.location.first_column - 1,
+              data.location.last_line - 1,
+              data.location.last_column - 1
+            );
+            editor.getSession().getDocument().replace(range, data.text);
+          }
+        );
+
+        if (initialCursorPosition.value) {
+          editor.moveCursorToPosition(initialCursorPosition.value);
+          editor.renderer.scrollCursorIntoView();
+        }
+
+        registerEditorCommands(editor, aceLocationHandler, triggerChange);
+        addInsertSubscribers(editor);
+        editorRef.value = editor;
+        emit('ace-created', editor);
+      });
+
+      subTracker.subscribe(
+        CURSOR_POSITION_CHANGED_EVENT,
+        (event: { editorId: string; position: Ace.Position }) => {
+          if (event.editorId === id.value) {
+            emit('cursor-changed', event.position);
+          }
+        }
+      );
+
+      subTracker.subscribe(
+        ACTIVE_STATEMENT_CHANGED_EVENT,
+        (details: ActiveStatementChangedEventDetails) => {
+          if (id.value === details.id) {
+            emit('active-statement-changed', details);
+          }
+        }
+      );
+
+      return { autocompleteParser, editorElement, subTracker, editor: editorRef, I18n };
     }
   });
 </script>