浏览代码

[editor] Fix syntax checker replacement in editor v2

Johan Ahlen 4 年之前
父节点
当前提交
2fafd839e8

+ 85 - 34
desktop/core/src/desktop/js/apps/editor/components/aceEditor/AceEditor.test.ts

@@ -20,45 +20,67 @@ 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 { AutocompleteParser, SqlParserProvider, SyntaxError, SyntaxParser } from 'parse/types';
+import { SetOptions, SqlReferenceProvider, UdfCategory } from 'sql/reference/types';
 import huePubSub from 'utils/huePubSub';
 import { nextTick } from 'vue';
 import AceEditor from './AceEditor.vue';
 
+import impalaSyntaxParser from 'parse/sql/impala/impalaSyntaxParser';
+import impalaAutocompleteParser from 'parse/sql/impala/impalaAutocompleteParser';
+
+const sqlParserProvider: SqlParserProvider = {
+  async getAutocompleteParser(): Promise<AutocompleteParser> {
+    return (impalaAutocompleteParser as unknown) as AutocompleteParser;
+  },
+
+  async getSyntaxParser(): Promise<SyntaxParser> {
+    return (impalaSyntaxParser as unknown) as SyntaxParser;
+  }
+};
+
+const sqlReferenceProvider: SqlReferenceProvider = {
+  getReservedKeywords(): Promise<Set<string>> {
+    return Promise.resolve(new Set<string>());
+  },
+  getSetOptions(): Promise<SetOptions> {
+    return Promise.resolve({});
+  },
+  getUdfCategories(): Promise<UdfCategory[]> {
+    return Promise.resolve([]);
+  },
+  hasUdfCategories(): boolean {
+    return false;
+  }
+};
+
 describe('AceEditor.vue', () => {
   const mockExecutor = ({
     connector: ko.observable({
-      dialect: 'foo',
-      id: 'foo'
+      dialect: 'impala',
+      id: 'impala'
     }),
     namespace: ko.observable({
       id: 'foo'
     }),
     compute: ko.observable({
       id: 'foo'
-    })
+    }),
+    database: ko.observable('default')
   } as unknown) as Executor;
 
-  it('should render', () => {
-    spyOn(dataCatalog, 'getChildren').and.returnValue(CancellablePromise.resolve([]));
-
-    const wrapper = shallowMount(AceEditor, {
-      props: {
-        value: 'some query',
-        id: 'some-id',
-        executor: mockExecutor
-      }
-    });
-    expect(wrapper.element).toMatchSnapshot();
-  });
-
-  it('should handle drag and drop pubsub event targeting this editor', async () => {
+  const shallowMountForEditor = async (
+    initialValue?: string
+  ): Promise<{ element: Element; editor: Ace.Editor }> => {
     spyOn(dataCatalog, 'getChildren').and.returnValue(CancellablePromise.resolve([]));
 
     const wrapper = shallowMount(AceEditor, {
       props: {
-        value: '',
+        initialValue,
         id: 'some-id',
-        executor: mockExecutor
+        executor: mockExecutor,
+        sqlParserProvider,
+        sqlReferenceProvider
       }
     });
 
@@ -68,6 +90,17 @@ describe('AceEditor.vue', () => {
 
     const editor = (wrapper.emitted()['ace-created'][0] as Ace.Editor[])[0];
 
+    return { element: wrapper.element, editor };
+  };
+
+  it('should render', async () => {
+    const { element } = await shallowMountForEditor('some query');
+    expect(element).toMatchSnapshot();
+  });
+
+  it('should handle drag and drop pubsub event targeting this editor', async () => {
+    const { editor } = await shallowMountForEditor();
+
     const draggedText = 'Some dropped text';
     huePubSub.publish(INSERT_AT_CURSOR_EVENT, {
       text: draggedText,
@@ -79,21 +112,7 @@ describe('AceEditor.vue', () => {
   });
 
   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 { editor } = await shallowMountForEditor();
 
     const draggedText = 'Some dropped text';
     huePubSub.publish(INSERT_AT_CURSOR_EVENT, {
@@ -104,4 +123,36 @@ describe('AceEditor.vue', () => {
 
     expect(editor.getValue()).not.toEqual(draggedText);
   });
+
+  it('should adjust parser 1-based parser location to 0-based ace range for syntax errors', async () => {
+    const invalidTokenValue = 'slelect';
+    const { editor } = await shallowMountForEditor(invalidTokenValue);
+    expect(editor.getValue()).toEqual(invalidTokenValue);
+
+    // Add syntax error manually (normally through web socket)
+    const token = editor.session.getTokenAt(0, 0);
+    expect(token).toBeDefined();
+    expect(token!.value).toEqual(invalidTokenValue);
+    const syntaxError: SyntaxError = {
+      expected: [{ text: 'select', distance: 0 }],
+      loc: { first_line: 1, last_line: 1, first_column: 0, last_column: 7 },
+      text: invalidTokenValue
+    };
+    token!.syntaxError = syntaxError;
+
+    // Listen for the dropdown show event
+    let syntaxDropdownCalled = false;
+    const subscription = huePubSub.subscribe('sql.syntax.dropdown.show', details => {
+      expect(details.range.start.row).toEqual(syntaxError.loc.first_line - 1);
+      expect(details.range.end.row).toEqual(syntaxError.loc.last_line - 1);
+      expect(details.data).toEqual(syntaxError);
+      syntaxDropdownCalled = true;
+    });
+
+    // Trigger right click in the editor
+    editor.container.dispatchEvent(new MouseEvent('contextmenu', { clientX: 1, clientY: 1 }));
+    subscription.remove();
+
+    expect(syntaxDropdownCalled).toBeTruthy();
+  });
 });

+ 18 - 12
desktop/core/src/desktop/js/apps/editor/components/aceEditor/AceLocationHandler.ts

@@ -431,16 +431,22 @@ export default class AceLocationHandler implements Disposable {
             ) === -1) ||
             token.syntaxError)
         ) {
-          const range = token.parseLocation
-            ? markLocation(token.parseLocation)
-            : new AceRange(
-                (token.syntaxError && token.syntaxError.loc.first_line - 1) || 1,
-                (token.syntaxError && token.syntaxError.loc.first_column) || 1,
-                (token.syntaxError && token.syntaxError.loc.last_line - 1) || 1,
-                (token.syntaxError &&
-                  token.syntaxError.loc.first_column + token.syntaxError.text.length) ||
-                  1
-              );
+          let range: Ace.Range | undefined = undefined;
+
+          if (token.parseLocation) {
+            range = markLocation(token.parseLocation);
+          } else if (token.syntaxError) {
+            range = new AceRange(
+              token.syntaxError.loc.first_line - 1,
+              token.syntaxError.loc.first_column,
+              token.syntaxError.loc.last_line - 1,
+              token.syntaxError.loc.first_column + token.syntaxError.text.length
+            );
+          }
+
+          if (!range) {
+            return;
+          }
 
           const startCoordinates = this.editor.renderer.textToScreenCoordinates(
             range.start.row,
@@ -511,10 +517,10 @@ export default class AceLocationHandler implements Disposable {
               editorId: this.editorId,
               data: token.syntaxError,
               editor: this.editor,
-              range: range,
+              range,
               sourceType: this.executor.connector().dialect,
               defaultDatabase: this.executor.database(),
-              source: source
+              source
             });
           }
           e.preventDefault();

+ 7 - 6
desktop/core/src/desktop/js/apps/editor/components/aceEditor/__snapshots__/AceEditor.test.ts.snap

@@ -3,7 +3,6 @@
 exports[`AceEditor.vue should render 1`] = `
 <div
   class="ace-editor-component"
-  value="some query"
 >
   <div
     class="ace-editor ace_editor ace-hue"
@@ -60,10 +59,6 @@ exports[`AceEditor.vue should render 1`] = `
           />
         </div>
       </div>
-      <div
-        class="ace_invisible ace_emptyMessage"
-        style="margin-left: 6px;"
-      />
     </div>
     <div
       class="ace_scrollbar ace_scrollbar-v"
@@ -96,6 +91,12 @@ exports[`AceEditor.vue should render 1`] = `
       </div>
     </div>
   </div>
-  <!--v-if-->
+  <ace-autocomplete-stub
+    autocomplete-parser="[object Object]"
+    editor="[object Object]"
+    editor-id="some-id"
+    executor="[object Object]"
+    sql-reference-provider="[object Object]"
+  />
 </div>
 `;