Эх сурвалжийг харах

[frontend] Remove snippet dependency from autocomplete logic in editor V2

Johan Ahlen 5 жил өмнө
parent
commit
0918867c73

+ 1 - 1
desktop/core/src/desktop/js/apps/notebook2/components/aceEditor/AceGutterHandler.ts

@@ -19,7 +19,7 @@ import SubscriptionTracker, { Disposable } from 'components/utils/SubscriptionTr
 import { Ace } from 'ext/ace';
 import { ParsedSqlStatement } from 'parse/sqlStatementsParser';
 import { EXECUTABLE_UPDATED_EVENT } from 'apps/notebook2/execution/executable';
-import { ACTIVE_STATEMENT_CHANGED_EVENT } from 'ko/bindings/ace/aceLocationHandler';
+import { ACTIVE_STATEMENT_CHANGED_EVENT } from './AceLocationHandler';
 import AceAnchoredRange from 'ko/bindings/ace/aceAnchoredRange';
 
 const LINE_BREAK_REGEX = /(\r\n)|(\n)|(\r)/g;

+ 25 - 11
desktop/core/src/desktop/js/apps/notebook2/components/aceEditor/AceLocationHandler.ts

@@ -23,7 +23,13 @@ import DataCatalogEntry from 'catalog/dataCatalogEntry';
 import SubscriptionTracker, { Disposable } from 'components/utils/SubscriptionTracker';
 import AssistStorageEntry from 'ko/components/assist/assistStorageEntry';
 import dataCatalog from 'catalog/dataCatalog';
-import { IdentifierChainEntry, IdentifierLocation, ParsedLocation, ParsedTable } from 'parse/types';
+import {
+  IdentifierChainEntry,
+  IdentifierLocation,
+  ParsedLocation,
+  ParsedTable,
+  SyntaxError
+} from 'parse/types';
 import { EditorInterpreter } from 'types/config';
 import { hueWindow } from 'types/types';
 import huePubSub, { HueSubscription } from 'utils/huePubSub';
@@ -39,9 +45,20 @@ import {
   POST_TO_SYNTAX_WORKER_EVENT
 } from 'sql/sqlWorkerHandler';
 
+export interface ActiveStatementChangedEvent {
+  id: string;
+  editorChangeTime: number;
+  activeStatementIndex: number;
+  totalStatementCount: number;
+  precedingStatements: ParsedSqlStatement[];
+  activeStatement: ParsedSqlStatement;
+  selectedStatements: ParsedSqlStatement[];
+  followingStatements: ParsedSqlStatement[];
+}
 export const REFRESH_STATEMENT_LOCATIONS_EVENT = 'editor.refresh.statement.locations';
 export const ACTIVE_STATEMENT_CHANGED_EVENT = 'editor.active.statement.changed';
 export const CURSOR_POSITION_CHANGED_EVENT = 'editor.cursor.position.changed';
+export const GET_ACTIVE_LOCATIONS_EVENT = 'get.active.editor.locations';
 
 const STATEMENT_COUNT_AROUND_ACTIVE = 10;
 
@@ -121,12 +138,12 @@ export default class AceLocationHandler implements Disposable {
         connector: this.executor.connector(),
         namespace: this.executor.namespace(),
         compute: this.executor.compute(),
-        path: []
+        path: <string[]>[]
       })
       .then(children => {
         this.availableDatabases.clear();
         children.forEach((dbEntry: DataCatalogEntry) => {
-          this.availableDatabases.add(dbEntry.getDisplayName(false).toLowerCase());
+          this.availableDatabases.add(dbEntry.name.toLowerCase());
         });
       });
   }
@@ -451,7 +468,7 @@ export default class AceLocationHandler implements Disposable {
             // Asterisk, function etc.
             if (parseLocation.type === 'file' && parseLocation.path) {
               AssistStorageEntry.getEntry(parseLocation.path).then(entry => {
-                entry.open(true);
+                (<{ open: KnockoutObservable<boolean> }>(<unknown>entry)).open(true);
                 huePubSub.publish('context.popover.show', {
                   data: {
                     type: 'storageEntry',
@@ -599,7 +616,7 @@ export default class AceLocationHandler implements Disposable {
         selectedStatements.push(activeStatement);
       }
 
-      huePubSub.publish(ACTIVE_STATEMENT_CHANGED_EVENT, {
+      huePubSub.publish(ACTIVE_STATEMENT_CHANGED_EVENT, <ActiveStatementChangedEvent>{
         id: this.editorId,
         editorChangeTime: lastKnownStatements.editorChangeTime,
         activeStatementIndex: statementIndex,
@@ -1007,10 +1024,7 @@ export default class AceLocationHandler implements Disposable {
                   const containsColumn = entries.some(
                     entry =>
                       location.identifierChain &&
-                      sqlUtils.identifierEquals(
-                        entry.getDisplayName(false),
-                        location.identifierChain[0].name
-                      )
+                      sqlUtils.identifierEquals(entry.name, location.identifierChain[0].name)
                   );
 
                   if (containsColumn) {
@@ -1151,7 +1165,7 @@ export default class AceLocationHandler implements Disposable {
               weightedExpected.sort((a, b) =>
                 a.distance === b.distance ? a.text.localeCompare(b.text) : a.distance - b.distance
               );
-              token.syntaxError = {
+              token.syntaxError = <SyntaxError>{
                 loc: token.parseLocation.location,
                 text: token.value,
                 expected: weightedExpected.slice(0, 50)
@@ -1210,7 +1224,7 @@ export default class AceLocationHandler implements Disposable {
 
     let lastKnownLocations = {};
 
-    this.subTracker.subscribe('get.active.editor.locations', callback => {
+    this.subTracker.subscribe(GET_ACTIVE_LOCATIONS_EVENT, callback => {
       callback(lastKnownLocations);
     });
 

+ 3 - 8
desktop/core/src/desktop/js/apps/notebook2/components/aceEditor/autocomplete/AutocompleteResults.ts

@@ -62,12 +62,12 @@ import {
 const COLORS = {
   POPULAR: '#61bbff',
   KEYWORD: '#0074d2',
-  COLUMN: '#4ccf4c',
+  COLUMN: '#2fae2f',
   TABLE: '#ffa139',
   DATABASE: '#517989',
   SAMPLE: '#fea7a7',
   IDENT_CTE_VAR: '#ca4f01',
-  UDF: '#19323c',
+  UDF: '#acfbac',
   HDFS: '#9e1414'
 };
 
@@ -346,12 +346,7 @@ class AutocompleteResults {
   loadingPopularTables = false;
   loadingPopularColumns = false;
 
-  constructor(options: {
-    executor: Executor;
-    editor: Ace.Editor;
-    temporaryOnly: boolean;
-    databaseObservable: () => string;
-  }) {
+  constructor(options: { executor: Executor; editor: Ace.Editor; temporaryOnly: boolean }) {
     this.executor = options.executor;
     this.editor = options.editor;
     this.temporaryOnly = options.temporaryOnly;

+ 201 - 0
desktop/core/src/desktop/js/apps/notebook2/components/aceEditor/autocomplete/SqlAutocompleter.ts

@@ -0,0 +1,201 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {
+  ACTIVE_STATEMENT_CHANGED_EVENT,
+  ActiveStatementChangedEvent,
+  GET_ACTIVE_LOCATIONS_EVENT,
+  REFRESH_STATEMENT_LOCATIONS_EVENT
+} from '../AceLocationHandler';
+import Executor from 'apps/notebook2/execution/executor';
+import SubscriptionTracker, { Disposable } from 'components/utils/SubscriptionTracker';
+import { Ace } from 'ext/ace';
+import { ParsedSqlStatement } from 'parse/sqlStatementsParser';
+import { AutocompleteParser, AutocompleteParseResult } from 'parse/types';
+import { EditorInterpreter } from 'types/config';
+import AutocompleteResults from './AutocompleteResults';
+import hueDebug from 'utils/hueDebug';
+import huePubSub from 'utils/huePubSub';
+import sqlParserRepository from 'parse/sql/sqlParserRepository';
+
+export default class SqlAutocompleter implements Disposable {
+  editor: Ace.Editor;
+  executor: Executor;
+  fixedPrefix: () => string;
+  fixedPostfix: () => string;
+  autocompleteResults: AutocompleteResults;
+  editorId: string;
+
+  subTracker = new SubscriptionTracker();
+  activeStatement: ParsedSqlStatement | null = null;
+
+  onPartial?: (partial: string) => void;
+
+  constructor(options: {
+    editorId: string;
+    executor: Executor;
+    temporaryOnly?: boolean;
+    editor: Ace.Editor;
+    fixedPrefix?: () => string;
+    fixedPostfix?: () => string;
+  }) {
+    this.editorId = options.editorId;
+    this.editor = options.editor;
+    this.executor = options.executor;
+    this.fixedPrefix = options.fixedPrefix || (() => '');
+    this.fixedPostfix = options.fixedPrefix || (() => '');
+
+    this.autocompleteResults = new AutocompleteResults({
+      executor: options.executor,
+      editor: this.editor,
+      temporaryOnly: !!options.temporaryOnly
+    });
+
+    this.subTracker.subscribe(
+      ACTIVE_STATEMENT_CHANGED_EVENT,
+      (event: ActiveStatementChangedEvent) => {
+        if (event.id === this.editorId) {
+          this.activeStatement = event.activeStatement;
+        }
+      }
+    );
+  }
+
+  getDialect(): string {
+    return (<EditorInterpreter>this.executor.connector()).dialect;
+  }
+
+  async parseActiveStatement(): Promise<AutocompleteParseResult> {
+    return new Promise((resolve, reject) => {
+      if (this.activeStatement) {
+        const activeStatementLocation = this.activeStatement.location;
+        const cursorPosition = this.editor.getCursorPosition();
+
+        if (
+          (activeStatementLocation.first_line - 1 < cursorPosition.row ||
+            (activeStatementLocation.first_line - 1 === cursorPosition.row &&
+              activeStatementLocation.first_column <= cursorPosition.column)) &&
+          (activeStatementLocation.last_line - 1 > cursorPosition.row ||
+            (activeStatementLocation.last_line - 1 === cursorPosition.row &&
+              activeStatementLocation.last_column >= cursorPosition.column))
+        ) {
+          const beforeCursor =
+            this.fixedPrefix() +
+            this.editor.session.getTextRange({
+              start: {
+                row: activeStatementLocation.first_line - 1,
+                column: activeStatementLocation.first_column
+              },
+              end: cursorPosition
+            });
+          const afterCursor =
+            this.editor.session.getTextRange({
+              start: cursorPosition,
+              end: {
+                row: activeStatementLocation.last_line - 1,
+                column: activeStatementLocation.last_column
+              }
+            }) + this.fixedPostfix();
+          const parserPromise = <Promise<AutocompleteParser>>(
+            sqlParserRepository.getAutocompleter(this.getDialect())
+          );
+          parserPromise
+            .then(autocompleteParser => {
+              resolve(autocompleteParser.parseSql(beforeCursor, afterCursor));
+            })
+            .catch(err => {
+              console.warn(err);
+              reject(err);
+            });
+        } else {
+          resolve();
+        }
+      } else {
+        resolve();
+      }
+    });
+  }
+
+  async parseAll(): Promise<AutocompleteParseResult> {
+    return new Promise((resolve, reject) => {
+      const parserPromise = <Promise<AutocompleteParser>>(
+        sqlParserRepository.getAutocompleter(this.getDialect())
+      );
+      parserPromise
+        .then(autocompleteParser => {
+          resolve(
+            autocompleteParser.parseSql(
+              this.editor.getTextBeforeCursor(),
+              this.editor.getTextAfterCursor()
+            )
+          );
+        })
+        .catch(reject);
+    });
+  }
+
+  async autocomplete(): Promise<void> {
+    let parseResult;
+    try {
+      huePubSub.publish(GET_ACTIVE_LOCATIONS_EVENT, (locations: ActiveStatementChangedEvent) => {
+        // This could happen in case the user is editing at the borders of the statement and the locations haven't
+        // been updated yet, in that case we have to force a location update before parsing
+        if (locations.editorChangeTime !== this.editor.lastChangeTime) {
+          huePubSub.publish(REFRESH_STATEMENT_LOCATIONS_EVENT, this.editorId);
+        }
+      });
+
+      parseResult = await this.parseActiveStatement();
+    } catch (e) {
+      if (typeof console.warn !== 'undefined') {
+        console.warn(e);
+      }
+    }
+
+    // In the unlikely case the statement parser fails we fall back to parsing all of it
+    if (!parseResult) {
+      try {
+        parseResult = await this.parseAll();
+      } catch (e) {
+        if (typeof console.warn !== 'undefined') {
+          console.warn(e);
+        }
+      }
+    }
+
+    if (!parseResult) {
+      // This prevents Ace from inserting garbled text in case of exception
+      huePubSub.publish('hue.ace.autocompleter.done');
+    } else {
+      if (typeof hueDebug !== 'undefined' && hueDebug.showParseResult) {
+        // eslint-disable-next-line no-restricted-syntax
+        console.log(parseResult);
+      }
+      try {
+        await this.autocompleteResults.update(parseResult);
+      } catch (e) {
+        if (typeof console.warn !== 'undefined') {
+          console.warn(e);
+        }
+        huePubSub.publish('hue.ace.autocompleter.done');
+      }
+    }
+  }
+
+  dispose(): void {
+    this.subTracker.dispose();
+  }
+}

+ 1 - 1
desktop/core/src/desktop/js/ko/bindings/ace/ko.aceEditor.js

@@ -23,7 +23,7 @@ import AceLocationHandler, {
   REFRESH_STATEMENT_LOCATIONS_EVENT
 } from 'ko/bindings/ace/aceLocationHandler';
 
-import AceLocationHandlerV2 from 'apps/notebook2/components/aceEditor/aceLocationHandler';
+import AceLocationHandlerV2 from 'apps/notebook2/components/aceEditor/AceLocationHandler';
 
 import huePubSub from 'utils/huePubSub';
 import AceGutterHandler from 'ko/bindings/ace/aceGutterHandler';