Browse Source

[editor] Fix Vue reactivity in the Ace Autocomplete component

Johan Ahlen 5 years ago
parent
commit
da54289b65

+ 196 - 150
desktop/core/src/desktop/js/apps/notebook2/components/aceEditor/autocomplete/AceAutocomplete.vue

@@ -24,37 +24,30 @@
     :style="{ top: top + 'px', left: left + 'px' }"
     :style="{ top: top + 'px', left: left + 'px' }"
   >
   >
     <div class="autocompleter-suggestions">
     <div class="autocompleter-suggestions">
-      <div
-        v-if="autocompleteResults.availableCategories.length > 1 || autocompleteResults.loading"
-        class="autocompleter-header"
-      >
+      <div v-if="availableCategories.length > 1 || loading" class="autocompleter-header">
         <!-- ko if: suggestions.availableCategories().length > 1 -->
         <!-- ko if: suggestions.availableCategories().length > 1 -->
-        <div
-          v-if="autocompleteResults.availableCategories.length > 1"
-          class="autocompleter-categories"
-        >
+        <div v-if="availableCategories.length > 1" class="autocompleter-categories">
           <div
           <div
-            v-for="category in autocompleteResults.availableCategories"
+            v-for="category in availableCategories"
             :key="category.label"
             :key="category.label"
             :style="{
             :style="{
-              'border-color':
-                autocompleteResults.activeCategory === category ? category.color : 'transparent'
+              'border-color': activeCategory === category ? category.color : 'transparent'
             }"
             }"
-            :class="{ active: autocompleteResults.activeCategory === category }"
+            :class="{ active: activeCategory === category }"
             @click="categoryClick(category, $event)"
             @click="categoryClick(category, $event)"
           >
           >
             {{ category.label }}
             {{ category.label }}
           </div>
           </div>
         </div>
         </div>
         <div class="autocompleter-spinner">
         <div class="autocompleter-spinner">
-          <spinner :spin="autocompleteResults.loading" size="small" />
+          <spinner :spin="loading" size="small" />
         </div>
         </div>
       </div>
       </div>
       <div class="autocompleter-entries">
       <div class="autocompleter-entries">
         <div>
         <div>
           <div
           <div
-            v-for="(suggestion, index) in autocompleteResults.filtered"
-            :key="suggestion.category.label + suggestion.value"
+            v-for="(suggestion, index) in filtered"
+            :key="activeCategory.categoryId + suggestion.category.categoryId + suggestion.value"
             class="autocompleter-suggestion"
             class="autocompleter-suggestion"
             :class="{ selected: index === selectedIndex }"
             :class="{ selected: index === selectedIndex }"
             @click="clickToInsert(index)"
             @click="clickToInsert(index)"
@@ -66,7 +59,7 @@
                 class="autocompleter-dot"
                 class="autocompleter-dot"
                 :style="{ 'background-color': suggestion.category.color }"
                 :style="{ 'background-color': suggestion.category.color }"
               />
               />
-              <matched-text :suggestion="suggestion" :filter="autocompleteResults.filter" />
+              <matched-text :suggestion="suggestion" :filter="filter" />
               <i v-if="suggestion.details && suggestion.details.primary_key" class="fa fa-key" />
               <i v-if="suggestion.details && suggestion.details.primary_key" class="fa fa-key" />
             </div>
             </div>
             <div class="autocompleter-suggestion-meta">
             <div class="autocompleter-suggestion-meta">
@@ -81,7 +74,10 @@
 </template>
 </template>
 
 
 <script lang="ts">
 <script lang="ts">
-  import MatchedText from 'apps/notebook2/components/aceEditor/autocomplete/MatchedText.vue';
+  import sqlUtils, { SortOverride } from 'sql/sqlUtils';
+  import huePubSub from 'utils/huePubSub';
+  import { Category, CategoryId, CategoryInfo, extractCategories } from './Category';
+  import MatchedText from './MatchedText.vue';
   import Executor from 'apps/notebook2/execution/executor';
   import Executor from 'apps/notebook2/execution/executor';
   import { clickOutsideDirective } from 'components/directives/clickOutsideDirective';
   import { clickOutsideDirective } from 'components/directives/clickOutsideDirective';
   import Spinner from 'components/Spinner.vue';
   import Spinner from 'components/Spinner.vue';
@@ -91,7 +87,7 @@
   import SqlAutocompleter from './SqlAutocompleter';
   import SqlAutocompleter from './SqlAutocompleter';
   import { defer } from 'utils/hueUtils';
   import { defer } from 'utils/hueUtils';
   import { Prop } from 'vue-property-decorator';
   import { Prop } from 'vue-property-decorator';
-  import AutocompleteResults, { Category } from './AutocompleteResults';
+  import AutocompleteResults, { Suggestion } from './AutocompleteResults';
   import Vue from 'vue';
   import Vue from 'vue';
   import Component from 'vue-class-component';
   import Component from 'vue-class-component';
 
 
@@ -117,12 +113,17 @@
     @Prop({ required: false, default: false })
     @Prop({ required: false, default: false })
     temporaryOnly?: boolean;
     temporaryOnly?: boolean;
 
 
+    filter = '';
+    availableCategories = [Category.All];
+    activeCategory = Category.All;
+
     active = false;
     active = false;
     left = 0;
     left = 0;
     top = 0;
     top = 0;
     selectedIndex: number | null = null;
     selectedIndex: number | null = null;
     hoveredIndex: number | null = null;
     hoveredIndex: number | null = null;
     base: Ace.Anchor | null = null;
     base: Ace.Anchor | null = null;
+    sortOverride?: SortOverride | null = null;
 
 
     autocompleter?: SqlAutocompleter;
     autocompleter?: SqlAutocompleter;
     autocompleteResults?: AutocompleteResults;
     autocompleteResults?: AutocompleteResults;
@@ -137,13 +138,14 @@
 
 
     subTracker = new SubscriptionTracker();
     subTracker = new SubscriptionTracker();
 
 
-    // TODO: Move filter, filtered, categories, activeCategory from AutocompleteResults to this component
-
     created(): void {
     created(): void {
       this.keyboardHandler = new HashHandler();
       this.keyboardHandler = new HashHandler();
       this.registerKeybindings(this.keyboardHandler);
       this.registerKeybindings(this.keyboardHandler);
 
 
       this.changeListener = () => {
       this.changeListener = () => {
+        if (!this.autocompleteResults) {
+          return;
+        }
         window.clearTimeout(this.changeTimeout);
         window.clearTimeout(this.changeTimeout);
         const cursor = this.editor.selection.lead;
         const cursor = this.editor.selection.lead;
         if (this.base && (cursor.row !== this.base.row || cursor.column < this.base.column)) {
         if (this.base && (cursor.row !== this.base.row || cursor.column < this.base.column)) {
@@ -162,18 +164,10 @@
                 )
                 )
               );
               );
             }
             }
-            this.autocompleteResults.filter = this.editor.session.getTextRange({
-              start: this.base,
-              end: pos
-            });
-
+            this.updateFilter();
             this.positionAutocompleteDropdown();
             this.positionAutocompleteDropdown();
 
 
-            // TODO: Vue does not react on changes to autocompleteResults.filter could be because we initialize it
-            // in mounted
-            this.$forceUpdate();
-
-            if (!this.autocompleteResults.filtered.length) {
+            if (!this.filtered.length) {
               this.detach();
               this.detach();
             }
             }
           }, 200);
           }, 200);
@@ -181,13 +175,163 @@
       };
       };
     }
     }
 
 
+    mounted(): void {
+      this.autocompleter = new SqlAutocompleter({
+        editorId: this.editorId,
+        executor: this.executor,
+        editor: this.editor,
+        temporaryOnly: this.temporaryOnly
+      });
+
+      this.subTracker.addDisposable(this.autocompleter);
+
+      this.autocompleteResults = this.autocompleter.autocompleteResults;
+
+      this.subTracker.subscribe('hue.ace.autocompleter.done', () => {
+        defer(() => {
+          if (this.active && !this.filtered.length && !this.loading) {
+            this.detach();
+          }
+        });
+      });
+
+      this.subTracker.subscribe(
+        'hue.ace.autocompleter.show',
+        async (details: {
+          editor: Ace.Editor;
+          lineHeight: number;
+          position: { top: number; left: number };
+        }) => {
+          if (details.editor !== this.editor || !this.autocompleter) {
+            return;
+          }
+          const session = this.editor.getSession();
+          const pos = this.editor.getCursorPosition();
+          const line = session.getLine(pos.row);
+          const prefix = aceUtil.retrievePrecedingIdentifier(line, pos.column);
+          const newBase = session.doc.createAnchor(pos.row, pos.column - prefix.length);
+
+          this.positionAutocompleteDropdown();
+
+          const afterAutocomplete = () => {
+            newBase.$insertRight = true;
+            this.base = newBase;
+            if (this.autocompleteResults) {
+              this.filter = prefix;
+            }
+            this.active = true;
+            this.selectedIndex = 0;
+          };
+
+          if (
+            !this.active ||
+            !this.base ||
+            newBase.column !== this.base.column ||
+            newBase.row !== this.base.row
+          ) {
+            try {
+              await this.autocompleter.autocomplete();
+              afterAutocomplete();
+              this.attach();
+            } catch (err) {
+              afterAutocomplete();
+            }
+          } else {
+            afterAutocomplete();
+          }
+        }
+      );
+
+      this.subTracker.subscribe(
+        'editor.autocomplete.temporary.sort.override',
+        (sortOverride: SortOverride): void => {
+          this.sortOverride = sortOverride;
+        }
+      );
+    }
+
+    get loading(): boolean {
+      return (
+        !this.autocompleteResults ||
+        this.autocompleteResults.loadingKeywords ||
+        this.autocompleteResults.loadingFunctions ||
+        this.autocompleteResults.loadingDatabases ||
+        this.autocompleteResults.loadingTables ||
+        this.autocompleteResults.loadingColumns ||
+        this.autocompleteResults.loadingValues ||
+        this.autocompleteResults.loadingPaths ||
+        this.autocompleteResults.loadingJoins ||
+        this.autocompleteResults.loadingJoinConditions ||
+        this.autocompleteResults.loadingAggregateFunctions ||
+        this.autocompleteResults.loadingGroupBys ||
+        this.autocompleteResults.loadingOrderBys ||
+        this.autocompleteResults.loadingFilters ||
+        this.autocompleteResults.loadingPopularTables ||
+        this.autocompleteResults.loadingPopularColumns
+      );
+    }
+
+    updateFilter(): void {
+      if (this.base) {
+        const pos = this.editor.getCursorPosition();
+        this.filter = this.editor.session.getTextRange({
+          start: this.base,
+          end: pos
+        });
+      }
+    }
+
+    get filtered(): Suggestion[] {
+      if (!this.autocompleteResults) {
+        return [];
+      }
+
+      let result = this.autocompleteResults.entries;
+
+      if (this.filter) {
+        result = sqlUtils.autocompleteFilter(this.filter, result);
+        huePubSub.publish('hue.ace.autocompleter.match.updated');
+      }
+
+      const categories = extractCategories(result);
+      if (categories.indexOf(this.activeCategory) === -1) {
+        this.activeCategory = Category.All;
+      }
+      this.availableCategories = categories;
+
+      const activeCategory = this.activeCategory;
+
+      const categoriesCount = new Map<CategoryId, number>();
+
+      result = result.filter(suggestion => {
+        categoriesCount.set(
+          suggestion.category.categoryId,
+          (categoriesCount.get(suggestion.category.categoryId) || 0) + 1
+        );
+        if (
+          activeCategory !== Category.Popular &&
+          (categoriesCount.get(suggestion.category.categoryId) || 0) >= 10 &&
+          suggestion.category.popular
+        ) {
+          return false;
+        }
+        return (
+          activeCategory === Category.All ||
+          activeCategory === suggestion.category ||
+          (activeCategory === Category.Popular && suggestion.popular)
+        );
+      });
+
+      sqlUtils.sortSuggestions(result, this.filter, this.sortOverride);
+      this.sortOverride = undefined;
+
+      return result;
+    }
+
     registerKeybindings(keyboardHandler: Ace.HashHandler): void {
     registerKeybindings(keyboardHandler: Ace.HashHandler): void {
       keyboardHandler.bindKeys({
       keyboardHandler.bindKeys({
         Up: () => {
         Up: () => {
-          if (!this.autocompleteResults) {
-            return;
-          }
-          if (this.autocompleteResults.filtered.length <= 1) {
+          if (this.filtered.length <= 1) {
             this.detach();
             this.detach();
             this.editor.execCommand('golineup');
             this.editor.execCommand('golineup');
           } else if (this.selectedIndex) {
           } else if (this.selectedIndex) {
@@ -195,22 +339,16 @@
             this.hoveredIndex = null;
             this.hoveredIndex = null;
             this.scrollSelectionIntoView();
             this.scrollSelectionIntoView();
           } else {
           } else {
-            this.selectedIndex = this.autocompleteResults.filtered.length - 1;
+            this.selectedIndex = this.filtered.length - 1;
             this.hoveredIndex = null;
             this.hoveredIndex = null;
             this.scrollSelectionIntoView();
             this.scrollSelectionIntoView();
           }
           }
         },
         },
         Down: () => {
         Down: () => {
-          if (!this.autocompleteResults) {
-            return;
-          }
-          if (this.autocompleteResults.filtered.length <= 1) {
+          if (this.filtered.length <= 1) {
             this.detach();
             this.detach();
             this.editor.execCommand('golinedown');
             this.editor.execCommand('golinedown');
-          } else if (
-            this.selectedIndex !== null &&
-            this.selectedIndex < this.autocompleteResults.filtered.length - 1
-          ) {
+          } else if (this.selectedIndex !== null && this.selectedIndex < this.filtered.length - 1) {
             this.selectedIndex = this.selectedIndex + 1;
             this.selectedIndex = this.selectedIndex + 1;
             this.hoveredIndex = null;
             this.hoveredIndex = null;
             this.scrollSelectionIntoView();
             this.scrollSelectionIntoView();
@@ -221,10 +359,7 @@
           }
           }
         },
         },
         'Ctrl-Up|Ctrl-Home': () => {
         'Ctrl-Up|Ctrl-Home': () => {
-          if (!this.autocompleteResults) {
-            return;
-          }
-          if (this.autocompleteResults.filtered.length <= 1) {
+          if (this.filtered.length <= 1) {
             this.detach();
             this.detach();
             this.editor.execCommand('gotostart');
             this.editor.execCommand('gotostart');
           } else {
           } else {
@@ -234,14 +369,11 @@
           }
           }
         },
         },
         'Ctrl-Down|Ctrl-End': () => {
         'Ctrl-Down|Ctrl-End': () => {
-          if (!this.autocompleteResults) {
-            return;
-          }
-          if (this.autocompleteResults.filtered.length <= 1) {
+          if (this.filtered.length <= 1) {
             this.detach();
             this.detach();
             this.editor.execCommand('gotoend');
             this.editor.execCommand('gotoend');
-          } else if (this.autocompleteResults.filtered.length > 0) {
-            this.selectedIndex = this.autocompleteResults.filtered.length - 1;
+          } else if (this.filtered.length > 0) {
+            this.selectedIndex = this.filtered.length - 1;
             this.hoveredIndex = null;
             this.hoveredIndex = null;
             this.scrollSelectionIntoView();
             this.scrollSelectionIntoView();
           }
           }
@@ -273,13 +405,7 @@
     }
     }
 
 
     insertSuggestion(emptyCallback?: () => void): void {
     insertSuggestion(emptyCallback?: () => void): void {
-      if (!this.autocompleteResults) {
-        return;
-      }
-
-      const results = this.autocompleteResults;
-
-      if (this.selectedIndex === null || !results.filtered.length) {
+      if (this.selectedIndex === null || !this.filtered.length) {
         this.detach();
         this.detach();
         if (emptyCallback) {
         if (emptyCallback) {
           emptyCallback();
           emptyCallback();
@@ -287,20 +413,20 @@
         return;
         return;
       }
       }
 
 
-      const selectedSuggestion = results.filtered[this.selectedIndex];
+      const selectedSuggestion = this.filtered[this.selectedIndex];
       const valueToInsert = selectedSuggestion.value;
       const valueToInsert = selectedSuggestion.value;
 
 
       // Not always the case as we also match in comments
       // Not always the case as we also match in comments
-      if (valueToInsert.toLowerCase() === results.filter.toLowerCase()) {
+      if (valueToInsert.toLowerCase() === this.filter.toLowerCase()) {
         // Close the autocomplete when the user has typed a complete suggestion
         // Close the autocomplete when the user has typed a complete suggestion
         this.detach();
         this.detach();
         return;
         return;
       }
       }
 
 
-      if (results.filter) {
+      if (this.filter) {
         const ranges = this.editor.selection.getAllRanges();
         const ranges = this.editor.selection.getAllRanges();
         ranges.forEach(range => {
         ranges.forEach(range => {
-          range.start.column -= results.filter.length;
+          range.start.column -= this.filter.length;
           this.editor.session.remove(range);
           this.editor.session.remove(range);
         });
         });
       }
       }
@@ -311,79 +437,6 @@
       this.detach();
       this.detach();
     }
     }
 
 
-    mounted(): void {
-      this.autocompleter = new SqlAutocompleter({
-        editorId: this.editorId,
-        executor: this.executor,
-        editor: this.editor,
-        temporaryOnly: this.temporaryOnly
-      });
-
-      this.subTracker.addDisposable(this.autocompleter);
-
-      this.autocompleteResults = this.autocompleter.autocompleteResults;
-
-      this.subTracker.subscribe('hue.ace.autocompleter.done', () => {
-        defer(() => {
-          if (
-            this.active &&
-            (!this.autocompleteResults ||
-              ((!this.autocompleteResults.filtered || !this.autocompleteResults.filtered.length) &&
-                !this.autocompleteResults.loading))
-          ) {
-            this.detach();
-          }
-        });
-      });
-
-      this.subTracker.subscribe(
-        'hue.ace.autocompleter.show',
-        async (details: {
-          editor: Ace.Editor;
-          lineHeight: number;
-          position: { top: number; left: number };
-        }) => {
-          if (details.editor !== this.editor || !this.autocompleter) {
-            return;
-          }
-          const session = this.editor.getSession();
-          const pos = this.editor.getCursorPosition();
-          const line = session.getLine(pos.row);
-          const prefix = aceUtil.retrievePrecedingIdentifier(line, pos.column);
-          const newBase = session.doc.createAnchor(pos.row, pos.column - prefix.length);
-
-          this.positionAutocompleteDropdown();
-
-          const afterAutocomplete = () => {
-            newBase.$insertRight = true;
-            this.base = newBase;
-            if (this.autocompleteResults) {
-              this.autocompleteResults.filter = prefix;
-            }
-            this.active = true;
-            this.selectedIndex = 0;
-          };
-
-          if (
-            !this.active ||
-            !this.base ||
-            newBase.column !== this.base.column ||
-            newBase.row !== this.base.row
-          ) {
-            try {
-              await this.autocompleter.autocomplete();
-              afterAutocomplete();
-              this.attach();
-            } catch (err) {
-              afterAutocomplete();
-            }
-          } else {
-            afterAutocomplete();
-          }
-        }
-      );
-    }
-
     positionAutocompleteDropdown(): void {
     positionAutocompleteDropdown(): void {
       const renderer = this.editor.renderer;
       const renderer = this.editor.renderer;
       const newPos = renderer.$cursorLayer.getPixelPosition(undefined, true);
       const newPos = renderer.$cursorLayer.getPixelPosition(undefined, true);
@@ -394,13 +447,7 @@
     }
     }
 
 
     get visible(): boolean {
     get visible(): boolean {
-      return (
-        this.active &&
-        !!(
-          this.autocompleteResults &&
-          (this.autocompleteResults.loading || this.autocompleteResults.filtered.length)
-        )
-      );
+      return this.active && (this.loading || !!this.filtered.length);
     }
     }
 
 
     scrollSelectionIntoView(): void {
     scrollSelectionIntoView(): void {
@@ -409,10 +456,12 @@
 
 
     suggestionSelected(index: number): void {
     suggestionSelected(index: number): void {
       this.selectedIndex = index;
       this.selectedIndex = index;
-      //$parent.insertSuggestion(); $parent.editor().focus();
+      this.insertSuggestion();
+      this.editor.focus();
     }
     }
 
 
     attach(): void {
     attach(): void {
+      this.updateFilter();
       this.disposeEventHandlers();
       this.disposeEventHandlers();
 
 
       if (this.keyboardHandler) {
       if (this.keyboardHandler) {
@@ -447,14 +496,11 @@
       }, 300);
       }, 300);
     }
     }
 
 
-    categoryClick(category: Category, event: Event): void {
+    categoryClick(category: CategoryInfo, event: Event): void {
       if (!this.autocompleteResults) {
       if (!this.autocompleteResults) {
         return;
         return;
       }
       }
-      this.autocompleteResults.activeCategory = category;
-
-      // TODO: Why doesn't Vue reactivity pick up the change?
-      this.$forceUpdate();
+      this.activeCategory = category;
 
 
       event.stopPropagation();
       event.stopPropagation();
       this.editor.focus();
       this.editor.focus();

+ 83 - 356
desktop/core/src/desktop/js/apps/notebook2/components/aceEditor/autocomplete/AutocompleteResults.ts

@@ -15,6 +15,7 @@
 // limitations under the License.
 // limitations under the License.
 
 
 import CancellableJqPromise from 'api/cancellableJqPromise';
 import CancellableJqPromise from 'api/cancellableJqPromise';
+import { Category, CategoryInfo } from 'apps/notebook2/components/aceEditor/autocomplete/Category';
 import Executor from 'apps/notebook2/execution/executor';
 import Executor from 'apps/notebook2/execution/executor';
 import DataCatalogEntry, {
 import DataCatalogEntry, {
   Field,
   Field,
@@ -46,7 +47,7 @@ import { hueWindow } from 'types/types';
 import hueUtils from 'utils/hueUtils';
 import hueUtils from 'utils/hueUtils';
 import huePubSub from 'utils/huePubSub';
 import huePubSub from 'utils/huePubSub';
 import I18n from 'utils/i18n';
 import I18n from 'utils/i18n';
-import sqlUtils, { SortOverride } from 'sql/sqlUtils';
+import sqlUtils from 'sql/sqlUtils';
 import { matchesType } from 'sql/reference/typeUtils';
 import { matchesType } from 'sql/reference/typeUtils';
 import { DIALECT } from 'apps/notebook2/snippet';
 import { DIALECT } from 'apps/notebook2/snippet';
 import { cancelActiveRequest } from 'api/apiUtils';
 import { cancelActiveRequest } from 'api/apiUtils';
@@ -59,44 +60,9 @@ import {
   getSetOptions
   getSetOptions
 } from 'sql/reference/sqlReferenceRepository';
 } from 'sql/reference/sqlReferenceRepository';
 
 
-const COLORS = {
-  POPULAR: '#61bbff',
-  KEYWORD: '#0074d2',
-  COLUMN: '#2fae2f',
-  TABLE: '#ffa139',
-  DATABASE: '#517989',
-  SAMPLE: '#fea7a7',
-  IDENT_CTE_VAR: '#ca4f01',
-  UDF: '#acfbac',
-  HDFS: '#9e1414'
-};
-
-const META_I18n: { [type: string]: string } = {
-  aggregateFunction: I18n('aggregate'),
-  alias: I18n('alias'),
-  commonTableExpression: I18n('cte'),
-  database: I18n('database'),
-  dir: I18n('dir'),
-  filter: I18n('filter'),
-  groupBy: I18n('group by'),
-  join: I18n('join'),
-  joinCondition: I18n('condition'),
-  keyword: I18n('keyword'),
-  orderBy: I18n('order by'),
-  sample: I18n('sample'),
-  table: I18n('table'),
-  variable: I18n('variable'),
-  view: I18n('view'),
-  virtual: I18n('virtual')
-};
-
-export interface Category {
-  id: string;
-  color: string;
-  label: string;
-  detailsTemplate?: string;
-  weight?: number;
-  popular?: boolean;
+interface ColumnReference {
+  type: string;
+  samples?: (string | number)[];
 }
 }
 
 
 export interface ColRefKeywordDetails {
 export interface ColRefKeywordDetails {
@@ -108,6 +74,10 @@ export interface FileDetails {
   type: string;
   type: string;
 }
 }
 
 
+export interface CommentDetails {
+  comment: string;
+}
+
 export interface Suggestion {
 export interface Suggestion {
   value: string;
   value: string;
   filterValue?: string;
   filterValue?: string;
@@ -123,7 +93,7 @@ export interface Suggestion {
     | TopAggValue
     | TopAggValue
     | TopFilterValue
     | TopFilterValue
     | Field
     | Field
-    | { comment?: string }
+    | CommentDetails
     | { name: string };
     | { name: string };
   matchComment?: boolean;
   matchComment?: boolean;
   matchIndex?: number;
   matchIndex?: number;
@@ -131,7 +101,7 @@ export interface Suggestion {
   hasCatalogEntry?: boolean;
   hasCatalogEntry?: boolean;
   meta: string;
   meta: string;
   relativePopularity?: number;
   relativePopularity?: number;
-  category: Category;
+  category: CategoryInfo;
   table?: ParsedTable;
   table?: ParsedTable;
   popular?: boolean;
   popular?: boolean;
   tableName?: string;
   tableName?: string;
@@ -140,164 +110,23 @@ export interface Suggestion {
   isColumn?: boolean;
   isColumn?: boolean;
 }
 }
 
 
-const CATEGORIES: { [category: string]: Category } = {
-  ALL: {
-    id: 'all',
-    color: '#90ceff',
-    label: I18n('All')
-  },
-  POPULAR: {
-    id: 'popular',
-    color: COLORS.POPULAR,
-    label: I18n('Popular'),
-    popular: true
-  },
-  POPULAR_AGGREGATE: {
-    id: 'popularAggregate',
-    weight: 1500,
-    color: COLORS.POPULAR,
-    label: I18n('Popular'),
-    detailsTemplate: 'agg-udf',
-    popular: true
-  },
-  POPULAR_GROUP_BY: {
-    id: 'popularGroupBy',
-    weight: 1300,
-    color: COLORS.POPULAR,
-    label: I18n('Popular'),
-    detailsTemplate: 'group-by',
-    popular: true
-  },
-  POPULAR_ORDER_BY: {
-    id: 'popularOrderBy',
-    weight: 1200,
-    color: COLORS.POPULAR,
-    label: I18n('Popular'),
-    detailsTemplate: 'order-by',
-    popular: true
-  },
-  POPULAR_FILTER: {
-    id: 'popularFilter',
-    weight: 1400,
-    color: COLORS.POPULAR,
-    label: I18n('Popular'),
-    detailsTemplate: 'filter',
-    popular: true
-  },
-  POPULAR_ACTIVE_JOIN: {
-    id: 'popularActiveJoin',
-    weight: 1500,
-    color: COLORS.POPULAR,
-    label: I18n('Popular'),
-    detailsTemplate: 'join',
-    popular: true
-  },
-  POPULAR_JOIN_CONDITION: {
-    id: 'popularJoinCondition',
-    weight: 1500,
-    color: COLORS.POPULAR,
-    label: I18n('Popular'),
-    detailsTemplate: 'join-condition',
-    popular: true
-  },
-  COLUMN: {
-    id: 'column',
-    weight: 1000,
-    color: COLORS.COLUMN,
-    label: I18n('Columns'),
-    detailsTemplate: 'column'
-  },
-  SAMPLE: {
-    id: 'sample',
-    weight: 900,
-    color: COLORS.SAMPLE,
-    label: I18n('Samples'),
-    detailsTemplate: 'value'
-  },
-  IDENTIFIER: {
-    id: 'identifier',
-    weight: 800,
-    color: COLORS.IDENT_CTE_VAR,
-    label: I18n('Identifiers'),
-    detailsTemplate: 'identifier'
-  },
-  CTE: {
-    id: 'cte',
-    weight: 700,
-    color: COLORS.IDENT_CTE_VAR,
-    label: I18n('CTEs'),
-    detailsTemplate: 'cte'
-  },
-  TABLE: {
-    id: 'table',
-    weight: 600,
-    color: COLORS.TABLE,
-    label: I18n('Tables'),
-    detailsTemplate: 'table'
-  },
-  DATABASE: {
-    id: 'database',
-    weight: 500,
-    color: COLORS.DATABASE,
-    label: I18n('Databases'),
-    detailsTemplate: 'database'
-  },
-  UDF: {
-    id: 'udf',
-    weight: 400,
-    color: COLORS.UDF,
-    label: I18n('UDFs'),
-    detailsTemplate: 'udf'
-  },
-  OPTION: {
-    id: 'option',
-    weight: 400,
-    color: COLORS.UDF,
-    label: I18n('Options'),
-    detailsTemplate: 'option'
-  },
-  HDFS: {
-    id: 'hdfs',
-    weight: 300,
-    color: COLORS.HDFS,
-    label: I18n('Files'),
-    detailsTemplate: 'hdfs'
-  },
-  VIRTUAL_COLUMN: {
-    id: 'virtualColumn',
-    weight: 200,
-    color: COLORS.COLUMN,
-    label: I18n('Columns'),
-    detailsTemplate: 'column'
-  },
-  COLREF_KEYWORD: {
-    id: 'colrefKeyword',
-    weight: 100,
-    color: COLORS.KEYWORD,
-    label: I18n('Keywords'),
-    detailsTemplate: 'keyword'
-  },
-  VARIABLE: {
-    id: 'variable',
-    weight: 50,
-    color: COLORS.IDENT_CTE_VAR,
-    label: I18n('Variables'),
-    detailsTemplate: 'variable'
-  },
-  KEYWORD: {
-    id: 'keyword',
-    weight: 0,
-    color: COLORS.KEYWORD,
-    label: I18n('Keywords'),
-    detailsTemplate: 'keyword'
-  },
-  POPULAR_JOIN: {
-    id: 'popularJoin',
-    weight: 1500,
-    color: COLORS.POPULAR,
-    label: I18n('Popular'),
-    detailsTemplate: 'join'
-  }
+const MetaLabels = {
+  AggregateFunction: I18n('aggregate'),
+  Alias: I18n('alias'),
+  CTE: I18n('cte'),
+  Database: I18n('database'),
+  Dir: I18n('dir'),
+  Filter: I18n('filter'),
+  GroupBy: I18n('group by'),
+  Join: I18n('join'),
+  JoinCondition: I18n('condition'),
+  Keyword: I18n('keyword'),
+  OrderBy: I18n('order by'),
+  Sample: I18n('sample'),
+  Table: I18n('table'),
+  Variable: I18n('variable'),
+  View: I18n('view'),
+  Virtual: I18n('virtual')
 };
 };
 
 
 const locateSubQuery = (subQueries: SubQuery[], subQueryName: string): SubQuery | undefined => {
 const locateSubQuery = (subQueries: SubQuery[], subQueryName: string): SubQuery | undefined => {
@@ -308,11 +137,6 @@ const locateSubQuery = (subQueries: SubQuery[], subQueryName: string): SubQuery
   }
   }
 };
 };
 
 
-interface ColumnReference {
-  type: string;
-  samples?: (string | number)[];
-}
-
 class AutocompleteResults {
 class AutocompleteResults {
   executor: Executor;
   executor: Executor;
   editor: Ace.Editor;
   editor: Ace.Editor;
@@ -320,12 +144,8 @@ class AutocompleteResults {
   activeDatabase: string;
   activeDatabase: string;
 
 
   parseResult!: AutocompleteParseResult;
   parseResult!: AutocompleteParseResult;
-  sortOverride?: SortOverride;
   entries: Suggestion[] = [];
   entries: Suggestion[] = [];
   subTracker = new SubscriptionTracker();
   subTracker = new SubscriptionTracker();
-  filter = '';
-  availableCategories = [CATEGORIES.ALL];
-  activeCategory = CATEGORIES.ALL;
   onCancelFunctions: (() => void)[] = [];
   onCancelFunctions: (() => void)[] = [];
   lastKnownRequests: JQueryXHR[] = [];
   lastKnownRequests: JQueryXHR[] = [];
   cancellablePromises: CancellableJqPromise<unknown>[] = [];
   cancellablePromises: CancellableJqPromise<unknown>[] = [];
@@ -351,97 +171,6 @@ class AutocompleteResults {
     this.editor = options.editor;
     this.editor = options.editor;
     this.temporaryOnly = options.temporaryOnly;
     this.temporaryOnly = options.temporaryOnly;
     this.activeDatabase = this.executor.database();
     this.activeDatabase = this.executor.database();
-
-    this.subTracker.subscribe(
-      'editor.autocomplete.temporary.sort.override',
-      (sortOverride: SortOverride): void => {
-        this.sortOverride = sortOverride;
-      }
-    );
-  }
-
-  updateAvailableCategories(suggestions: Suggestion[]): void {
-    const categoriesIndex: typeof CATEGORIES = {};
-    suggestions.forEach(suggestion => {
-      if (suggestion.popular && !categoriesIndex.POPULAR) {
-        categoriesIndex.POPULAR = CATEGORIES.POPULAR;
-      } else if (suggestion.category === CATEGORIES.TABLE && !categoriesIndex.TABLE) {
-        categoriesIndex.TABLE = suggestion.category;
-      } else if (suggestion.category === CATEGORIES.COLUMN && !categoriesIndex.COLUMN) {
-        categoriesIndex.COLUMN = suggestion.category;
-      } else if (suggestion.category === CATEGORIES.UDF && !categoriesIndex.UDF) {
-        categoriesIndex.UDF = suggestion.category;
-      }
-    });
-    const categories: Category[] = [];
-    Object.keys(categoriesIndex).forEach(key => {
-      categories.push(categoriesIndex[key]);
-    });
-    categories.sort((a, b) => a.label.localeCompare(b.label));
-    categories.unshift(CATEGORIES.ALL);
-    if (categories.indexOf(this.activeCategory) === -1) {
-      this.activeCategory = CATEGORIES.ALL;
-    }
-    this.availableCategories = categories;
-  }
-
-  get filtered(): Suggestion[] {
-    let result = this.entries;
-
-    if (this.filter) {
-      result = sqlUtils.autocompleteFilter(this.filter, result);
-      huePubSub.publish('hue.ace.autocompleter.match.updated');
-    }
-
-    this.updateAvailableCategories(result);
-
-    const activeCategory = this.activeCategory;
-
-    const categoriesCount: { [id: string]: number } = {};
-
-    result = result.filter(suggestion => {
-      if (typeof categoriesCount[suggestion.category.id] === 'undefined') {
-        categoriesCount[suggestion.category.id] = 0;
-      } else {
-        categoriesCount[suggestion.category.id]++;
-      }
-      if (
-        activeCategory !== CATEGORIES.POPULAR &&
-        categoriesCount[suggestion.category.id] >= 10 &&
-        suggestion.category.popular
-      ) {
-        return false;
-      }
-      return (
-        activeCategory === CATEGORIES.ALL ||
-        activeCategory === suggestion.category ||
-        (activeCategory === CATEGORIES.POPULAR && suggestion.popular)
-      );
-    });
-
-    sqlUtils.sortSuggestions(result, this.filter, this.sortOverride);
-    this.sortOverride = undefined;
-    return result;
-  }
-
-  get loading(): boolean {
-    return (
-      this.loadingKeywords ||
-      this.loadingFunctions ||
-      this.loadingDatabases ||
-      this.loadingTables ||
-      this.loadingColumns ||
-      this.loadingValues ||
-      this.loadingPaths ||
-      this.loadingJoins ||
-      this.loadingJoinConditions ||
-      this.loadingAggregateFunctions ||
-      this.loadingGroupBys ||
-      this.loadingOrderBys ||
-      this.loadingFilters ||
-      this.loadingPopularTables ||
-      this.loadingPopularColumns
-    );
   }
   }
 
 
   dialect(): string {
   dialect(): string {
@@ -474,8 +203,6 @@ class AutocompleteResults {
     this.loadingPopularTables = false;
     this.loadingPopularTables = false;
     this.loadingPopularColumns = false;
     this.loadingPopularColumns = false;
 
 
-    this.filter = '';
-
     if (this.parseResult.udfArgument) {
     if (this.parseResult.udfArgument) {
       await this.adjustForUdfArgument();
       await this.adjustForUdfArgument();
     }
     }
@@ -650,8 +377,8 @@ class AutocompleteResults {
     }
     }
     return suggestKeywords.map(keyword => ({
     return suggestKeywords.map(keyword => ({
       value: this.parseResult.lowerCase ? keyword.value.toLowerCase() : keyword.value,
       value: this.parseResult.lowerCase ? keyword.value.toLowerCase() : keyword.value,
-      meta: META_I18n.keyword,
-      category: CATEGORIES.KEYWORD,
+      meta: MetaLabels.Keyword,
+      category: Category.Keyword,
       weightAdjust: keyword.weight,
       weightAdjust: keyword.weight,
       popular: false
       popular: false
     }));
     }));
@@ -671,8 +398,8 @@ class AutocompleteResults {
         suggestColRefKeywords[typeForKeywords].forEach(keyword => {
         suggestColRefKeywords[typeForKeywords].forEach(keyword => {
           colRefKeywordSuggestions.push({
           colRefKeywordSuggestions.push({
             value: this.parseResult.lowerCase ? keyword.toLowerCase() : keyword,
             value: this.parseResult.lowerCase ? keyword.toLowerCase() : keyword,
-            meta: META_I18n.keyword,
-            category: CATEGORIES.COLREF_KEYWORD,
+            meta: MetaLabels.Keyword,
+            category: Category.ColRefKeyword,
             popular: false,
             popular: false,
             details: {
             details: {
               type: colRef.type
               type: colRef.type
@@ -693,7 +420,7 @@ class AutocompleteResults {
     return suggestIdentifiers.map(identifier => ({
     return suggestIdentifiers.map(identifier => ({
       value: identifier.name,
       value: identifier.name,
       meta: identifier.type,
       meta: identifier.type,
-      category: CATEGORIES.IDENTIFIER,
+      category: Category.Identifier,
       popular: false
       popular: false
     }));
     }));
   }
   }
@@ -710,8 +437,8 @@ class AutocompleteResults {
       if (type === 'COLREF') {
       if (type === 'COLREF') {
         columnAliasSuggestions.push({
         columnAliasSuggestions.push({
           value: columnAlias.name,
           value: columnAlias.name,
-          meta: META_I18n.alias,
-          category: CATEGORIES.COLUMN,
+          meta: MetaLabels.Alias,
+          category: Category.Column,
           popular: false,
           popular: false,
           details: columnAlias
           details: columnAlias
         });
         });
@@ -722,7 +449,7 @@ class AutocompleteResults {
           columnAliasSuggestions.push({
           columnAliasSuggestions.push({
             value: columnAlias.name,
             value: columnAlias.name,
             meta: resolvedType,
             meta: resolvedType,
-            category: CATEGORIES.COLUMN,
+            category: Category.Column,
             popular: false,
             popular: false,
             details: columnAlias
             details: columnAlias
           });
           });
@@ -731,7 +458,7 @@ class AutocompleteResults {
         columnAliasSuggestions.push({
         columnAliasSuggestions.push({
           value: columnAlias.name,
           value: columnAlias.name,
           meta: type,
           meta: type,
-          category: CATEGORIES.COLUMN,
+          category: Category.Column,
           popular: false,
           popular: false,
           details: columnAlias
           details: columnAlias
         });
         });
@@ -755,8 +482,8 @@ class AutocompleteResults {
       commonTableExpressionSuggestions.push({
       commonTableExpressionSuggestions.push({
         value: prefix + expression.name,
         value: prefix + expression.name,
         filterValue: expression.name,
         filterValue: expression.name,
-        meta: META_I18n.commonTableExpression,
-        category: CATEGORIES.CTE,
+        meta: MetaLabels.CTE,
+        category: Category.CTE,
         popular: false
         popular: false
       });
       });
     });
     });
@@ -771,7 +498,7 @@ class AutocompleteResults {
     try {
     try {
       const setOptions = await getSetOptions(this.executor.connector());
       const setOptions = await getSetOptions(this.executor.connector());
       return Object.keys(setOptions).map(name => ({
       return Object.keys(setOptions).map(name => ({
-        category: CATEGORIES.OPTION,
+        category: Category.Option,
         value: name,
         value: name,
         meta: '',
         meta: '',
         popular: false,
         popular: false,
@@ -808,7 +535,7 @@ class AutocompleteResults {
 
 
           const functionSuggestions: Suggestion[] = [];
           const functionSuggestions: Suggestion[] = [];
           functionsToSuggest.map(udf => ({
           functionsToSuggest.map(udf => ({
-            category: CATEGORIES.UDF,
+            category: Category.UDF,
             value: udf.name + '()',
             value: udf.name + '()',
             meta: udf.returnTypes.join('|'),
             meta: udf.returnTypes.join('|'),
             weightAdjust:
             weightAdjust:
@@ -846,7 +573,7 @@ class AutocompleteResults {
           !!this.parseResult.suggestAnalyticFunctions
           !!this.parseResult.suggestAnalyticFunctions
         );
         );
         suggestions = functionsToSuggest.map(udf => ({
         suggestions = functionsToSuggest.map(udf => ({
-          category: CATEGORIES.UDF,
+          category: Category.UDF,
           value: udf.name + '()',
           value: udf.name + '()',
           meta: udf.returnTypes.join('|'),
           meta: udf.returnTypes.join('|'),
           weightAdjust:
           weightAdjust:
@@ -888,8 +615,8 @@ class AutocompleteResults {
               (await sqlUtils.backTickIfNeeded(this.executor.connector(), name)) +
               (await sqlUtils.backTickIfNeeded(this.executor.connector(), name)) +
               (suggestDatabases.appendDot ? '.' : ''),
               (suggestDatabases.appendDot ? '.' : ''),
             filterValue: name,
             filterValue: name,
-            meta: META_I18n.database,
-            category: CATEGORIES.DATABASE,
+            meta: MetaLabels.Database,
+            category: Category.Database,
             popular: false,
             popular: false,
             hasCatalogEntry: true,
             hasCatalogEntry: true,
             details: dbEntry
             details: dbEntry
@@ -956,8 +683,8 @@ class AutocompleteResults {
             value: prefix + (await sqlUtils.backTickIfNeeded(this.executor.connector(), name)),
             value: prefix + (await sqlUtils.backTickIfNeeded(this.executor.connector(), name)),
             filterValue: name,
             filterValue: name,
             tableName: name,
             tableName: name,
-            meta: META_I18n[tableEntry.getType().toLowerCase()],
-            category: CATEGORIES.TABLE,
+            meta: tableEntry.isView() ? MetaLabels.View : MetaLabels.Table,
+            category: Category.Table,
             popular: false,
             popular: false,
             hasCatalogEntry: true,
             hasCatalogEntry: true,
             details: tableEntry
             details: tableEntry
@@ -1048,15 +775,15 @@ class AutocompleteResults {
     if (this.dialect() === DIALECT.hive && /[^.]$/.test(this.editor.getTextBeforeCursor())) {
     if (this.dialect() === DIALECT.hive && /[^.]$/.test(this.editor.getTextBeforeCursor())) {
       columnSuggestions.push({
       columnSuggestions.push({
         value: 'BLOCK__OFFSET__INSIDE__FILE',
         value: 'BLOCK__OFFSET__INSIDE__FILE',
-        meta: META_I18n.virtual,
-        category: CATEGORIES.VIRTUAL_COLUMN,
+        meta: MetaLabels.Virtual,
+        category: Category.VirtualColumn,
         popular: false,
         popular: false,
         details: { name: 'BLOCK__OFFSET__INSIDE__FILE' }
         details: { name: 'BLOCK__OFFSET__INSIDE__FILE' }
       });
       });
       columnSuggestions.push({
       columnSuggestions.push({
         value: 'INPUT__FILE__NAME',
         value: 'INPUT__FILE__NAME',
-        meta: META_I18n.virtual,
-        category: CATEGORIES.VIRTUAL_COLUMN,
+        meta: MetaLabels.Virtual,
+        category: Category.VirtualColumn,
         popular: false,
         popular: false,
         details: { name: 'INPUT__FILE__NAME' }
         details: { name: 'INPUT__FILE__NAME' }
       });
       });
@@ -1084,7 +811,7 @@ class AutocompleteResults {
           value: await sqlUtils.backTickIfNeeded(this.executor.connector(), column.alias),
           value: await sqlUtils.backTickIfNeeded(this.executor.connector(), column.alias),
           filterValue: column.alias,
           filterValue: column.alias,
           meta: type,
           meta: type,
-          category: CATEGORIES.COLUMN,
+          category: Category.Column,
           table: table,
           table: table,
           popular: false,
           popular: false,
           details: column
           details: column
@@ -1101,7 +828,7 @@ class AutocompleteResults {
           ),
           ),
           filterValue: column.identifierChain[column.identifierChain.length - 1].name,
           filterValue: column.identifierChain[column.identifierChain.length - 1].name,
           meta: type,
           meta: type,
-          category: CATEGORIES.COLUMN,
+          category: Category.Column,
           table: table,
           table: table,
           popular: false,
           popular: false,
           details: column
           details: column
@@ -1131,7 +858,7 @@ class AutocompleteResults {
               value: await sqlUtils.backTickIfNeeded(connector, column.alias),
               value: await sqlUtils.backTickIfNeeded(connector, column.alias),
               filterValue: column.alias,
               filterValue: column.alias,
               meta: type,
               meta: type,
-              category: CATEGORIES.COLUMN,
+              category: Category.Column,
               table: table,
               table: table,
               popular: false,
               popular: false,
               details: column
               details: column
@@ -1144,7 +871,7 @@ class AutocompleteResults {
               ),
               ),
               filterValue: column.identifierChain[column.identifierChain.length - 1].name,
               filterValue: column.identifierChain[column.identifierChain.length - 1].name,
               meta: type,
               meta: type,
-              category: CATEGORIES.COLUMN,
+              category: Category.Column,
               table: table,
               table: table,
               popular: false,
               popular: false,
               details: column
               details: column
@@ -1222,7 +949,7 @@ class AutocompleteResults {
               value: name,
               value: name,
               meta: childEntry.getType(),
               meta: childEntry.getType(),
               table: table,
               table: table,
-              category: CATEGORIES.COLUMN,
+              category: Category.Column,
               popular: false,
               popular: false,
               weightAdjust:
               weightAdjust:
                 types[0].toUpperCase() !== 'T' &&
                 types[0].toUpperCase() !== 'T' &&
@@ -1261,7 +988,7 @@ class AutocompleteResults {
               value: field.name,
               value: field.name,
               meta: fieldType,
               meta: fieldType,
               table: table,
               table: table,
-              category: CATEGORIES.COLUMN,
+              category: Category.Column,
               popular: false,
               popular: false,
               weightAdjust:
               weightAdjust:
                 types[0].toUpperCase() !== 'T' &&
                 types[0].toUpperCase() !== 'T' &&
@@ -1345,8 +1072,8 @@ class AutocompleteResults {
       valueSuggestions.push({
       valueSuggestions.push({
         value:
         value:
           '${' + colRefResult.identifierChain[colRefResult.identifierChain.length - 1].name + '}',
           '${' + colRefResult.identifierChain[colRefResult.identifierChain.length - 1].name + '}',
-        meta: META_I18n.variable,
-        category: CATEGORIES.VARIABLE,
+        meta: MetaLabels.Variable,
+        category: Category.Variable,
         popular: false
         popular: false
       });
       });
     }
     }
@@ -1364,8 +1091,8 @@ class AutocompleteResults {
       colRef.samples.forEach(sample => {
       colRef.samples.forEach(sample => {
         valueSuggestions.push({
         valueSuggestions.push({
           value: isString ? startQuote + sample + endQuote : String(sample),
           value: isString ? startQuote + sample + endQuote : String(sample),
-          meta: META_I18n.sample,
-          category: CATEGORIES.SAMPLE,
+          meta: MetaLabels.Sample,
+          category: Category.Sample,
           popular: false
           popular: false
         });
         });
       });
       });
@@ -1389,36 +1116,36 @@ class AutocompleteResults {
       suggestions = [
       suggestions = [
         {
         {
           value: 'adl://',
           value: 'adl://',
-          meta: META_I18n.keyword,
-          category: CATEGORIES.KEYWORD,
+          meta: MetaLabels.Keyword,
+          category: Category.Keyword,
           weightAdjust: 0,
           weightAdjust: 0,
           popular: false
           popular: false
         },
         },
         {
         {
           value: 's3a://',
           value: 's3a://',
-          meta: META_I18n.keyword,
-          category: CATEGORIES.KEYWORD,
+          meta: MetaLabels.Keyword,
+          category: Category.Keyword,
           weightAdjust: 0,
           weightAdjust: 0,
           popular: false
           popular: false
         },
         },
         {
         {
           value: 'hdfs://',
           value: 'hdfs://',
-          meta: META_I18n.keyword,
-          category: CATEGORIES.KEYWORD,
+          meta: MetaLabels.Keyword,
+          category: Category.Keyword,
           weightAdjust: 0,
           weightAdjust: 0,
           popular: false
           popular: false
         },
         },
         {
         {
           value: 'abfs://',
           value: 'abfs://',
-          meta: META_I18n.keyword,
-          category: CATEGORIES.KEYWORD,
+          meta: MetaLabels.Keyword,
+          category: Category.Keyword,
           weightAdjust: 0,
           weightAdjust: 0,
           popular: false
           popular: false
         },
         },
         {
         {
           value: '/',
           value: '/',
-          meta: META_I18n.dir,
-          category: CATEGORIES.HDFS,
+          meta: MetaLabels.Dir,
+          category: Category.Files,
           popular: false
           popular: false
         }
         }
       ];
       ];
@@ -1443,7 +1170,7 @@ class AutocompleteResults {
           suggestions.push({
           suggestions.push({
             value: rootPath,
             value: rootPath,
             meta: 'abfs',
             meta: 'abfs',
-            category: CATEGORIES.HDFS,
+            category: Category.Files,
             weightAdjust: 0,
             weightAdjust: 0,
             popular: false
             popular: false
           });
           });
@@ -1475,7 +1202,7 @@ class AutocompleteResults {
                   suggestions.push({
                   suggestions.push({
                     value: path === '' ? '/' + file.name : file.name,
                     value: path === '' ? '/' + file.name : file.name,
                     meta: file.type,
                     meta: file.type,
-                    category: CATEGORIES.HDFS,
+                    category: Category.Files,
                     popular: false,
                     popular: false,
                     details: file
                     details: file
                   });
                   });
@@ -1611,10 +1338,10 @@ class AutocompleteResults {
             totalCount += value.totalQueryCount;
             totalCount += value.totalQueryCount;
             joinSuggestions.push({
             joinSuggestions.push({
               value: suggestionString,
               value: suggestionString,
-              meta: META_I18n.join,
+              meta: MetaLabels.Join,
               category: suggestJoins.prependJoin
               category: suggestJoins.prependJoin
-                ? CATEGORIES.POPULAR_JOIN
-                : CATEGORIES.POPULAR_ACTIVE_JOIN,
+                ? Category.PopularJoin
+                : Category.PopularActiveJoin,
               popular: true,
               popular: true,
               details: value
               details: value
             });
             });
@@ -1702,8 +1429,8 @@ class AutocompleteResults {
             totalCount += value.totalQueryCount;
             totalCount += value.totalQueryCount;
             joinConditionSuggestions.push({
             joinConditionSuggestions.push({
               value: suggestionString,
               value: suggestionString,
-              meta: META_I18n.joinCondition,
-              category: CATEGORIES.POPULAR_JOIN_CONDITION,
+              meta: MetaLabels.JoinCondition,
+              category: Category.PopularJoinCondition,
               popular: true,
               popular: true,
               details: value
               details: value
             });
             });
@@ -1831,7 +1558,7 @@ class AutocompleteResults {
         aggregateFunctionsSuggestions.push({
         aggregateFunctionsSuggestions.push({
           value: clean,
           value: clean,
           meta: (value.function && value.function.returnTypes.join('|')) || '',
           meta: (value.function && value.function.returnTypes.join('|')) || '',
-          category: CATEGORIES.POPULAR_AGGREGATE,
+          category: Category.PopularAggregate,
           weightAdjust: Math.min(value.totalQueryCount, 99),
           weightAdjust: Math.min(value.totalQueryCount, 99),
           popular: true,
           popular: true,
           details: value
           details: value
@@ -1913,11 +1640,11 @@ class AutocompleteResults {
           suggestions.push({
           suggestions.push({
             value: prefix + filterValue,
             value: prefix + filterValue,
             filterValue: filterValue,
             filterValue: filterValue,
-            meta: optimizerAttribute === 'groupByColumn' ? META_I18n.groupBy : META_I18n.orderBy,
+            meta: optimizerAttribute === 'groupByColumn' ? MetaLabels.GroupBy : MetaLabels.OrderBy,
             category:
             category:
               optimizerAttribute === 'groupByColumn'
               optimizerAttribute === 'groupByColumn'
-                ? CATEGORIES.POPULAR_GROUP_BY
-                : CATEGORIES.POPULAR_ORDER_BY,
+                ? Category.PopularGroupBy
+                : Category.PopularOrderBy,
             weightAdjust: Math.round(
             weightAdjust: Math.round(
               (100 *
               (100 *
                 (<{ columnCount: number }>entry.optimizerPopularity[optimizerAttribute])
                 (<{ columnCount: number }>entry.optimizerPopularity[optimizerAttribute])
@@ -2046,8 +1773,8 @@ class AutocompleteResults {
                   totalCount += popularValue.count;
                   totalCount += popularValue.count;
                   filterSuggestions.push({
                   filterSuggestions.push({
                     value: compVal,
                     value: compVal,
-                    meta: META_I18n.filter,
-                    category: CATEGORIES.POPULAR_FILTER,
+                    meta: MetaLabels.Filter,
+                    category: Category.PopularFilter,
                     popular: false,
                     popular: false,
                     details: popularValue
                     details: popularValue
                   });
                   });

+ 222 - 0
desktop/core/src/desktop/js/apps/notebook2/components/aceEditor/autocomplete/Category.ts

@@ -0,0 +1,222 @@
+// 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 { Suggestion } from './AutocompleteResults';
+import I18n from 'utils/i18n';
+
+export interface CategoryInfo {
+  categoryId: CategoryId;
+  color: string;
+  label: string;
+  weight?: number;
+  popular?: boolean;
+}
+
+enum Colors {
+  Popular = '#61bbff',
+  Keyword = '#0074d2',
+  Column = '#2fae2f',
+  Table = '#ffa139',
+  Database = '#517989',
+  Sample = '#fea7a7',
+  IdentCteVar = '#ca4f01',
+  UDF = '#acfbac',
+  Files = '#9e1414'
+}
+
+export enum CategoryId {
+  All = 'All',
+  ColRefKeyword = 'ColRefKeyword',
+  Column = 'Column',
+  CTE = 'CTE',
+  Database = 'Database',
+  Files = 'Files',
+  Identifier = 'Identifier',
+  Keyword = 'Keyword',
+  Option = 'Option',
+  Popular = 'Popular',
+  PopularActiveJoin = 'PopularActiveJoin',
+  PopularAggregate = 'PopularAggregate',
+  PopularFilter = 'PopularFilter',
+  PopularGroupBy = 'PopularGroupBy',
+  PopularJoin = 'PopularJoin',
+  PopularJoinCondition = 'PopularJoinCondition',
+  PopularOrderBy = 'PopularOrderBy',
+  Sample = 'Sample',
+  Table = 'Table',
+  UDF = 'UDF',
+  Variable = 'Variable',
+  VirtualColumn = 'VirtualColumn'
+}
+
+export const Category: { [categoryId in keyof typeof CategoryId]: CategoryInfo } = {
+  All: {
+    categoryId: CategoryId.All, // TODO: used?
+    color: '#90ceff',
+    label: I18n('All')
+  },
+  Popular: {
+    categoryId: CategoryId.Popular,
+    color: Colors.Popular,
+    label: I18n('Popular'),
+    popular: true
+  },
+  PopularAggregate: {
+    categoryId: CategoryId.PopularAggregate,
+    weight: 1500,
+    color: Colors.Popular,
+    label: I18n('Popular'),
+    popular: true
+  },
+  PopularGroupBy: {
+    categoryId: CategoryId.PopularGroupBy,
+    weight: 1300,
+    color: Colors.Popular,
+    label: I18n('Popular'),
+    popular: true
+  },
+  PopularOrderBy: {
+    categoryId: CategoryId.PopularOrderBy,
+    weight: 1200,
+    color: Colors.Popular,
+    label: I18n('Popular'),
+    popular: true
+  },
+  PopularFilter: {
+    categoryId: CategoryId.PopularFilter,
+    weight: 1400,
+    color: Colors.Popular,
+    label: I18n('Popular'),
+    popular: true
+  },
+  PopularActiveJoin: {
+    categoryId: CategoryId.PopularActiveJoin,
+    weight: 1500,
+    color: Colors.Popular,
+    label: I18n('Popular'),
+    popular: true
+  },
+  PopularJoinCondition: {
+    categoryId: CategoryId.PopularJoinCondition,
+    weight: 1500,
+    color: Colors.Popular,
+    label: I18n('Popular'),
+    popular: true
+  },
+  Column: {
+    categoryId: CategoryId.Column,
+    weight: 1000,
+    color: Colors.Column,
+    label: I18n('Columns')
+  },
+  Sample: {
+    categoryId: CategoryId.Sample,
+    weight: 900,
+    color: Colors.Sample,
+    label: I18n('Samples')
+  },
+  Identifier: {
+    categoryId: CategoryId.Identifier,
+    weight: 800,
+    color: Colors.IdentCteVar,
+    label: I18n('Identifiers')
+  },
+  CTE: {
+    categoryId: CategoryId.CTE,
+    weight: 700,
+    color: Colors.IdentCteVar,
+    label: I18n('CTEs')
+  },
+  Table: {
+    categoryId: CategoryId.Table,
+    weight: 600,
+    color: Colors.Table,
+    label: I18n('Tables')
+  },
+  Database: {
+    categoryId: CategoryId.Database,
+    weight: 500,
+    color: Colors.Database,
+    label: I18n('Databases')
+  },
+  UDF: {
+    categoryId: CategoryId.UDF,
+    weight: 400,
+    color: Colors.UDF,
+    label: I18n('UDFs')
+  },
+  Option: {
+    categoryId: CategoryId.Option,
+    weight: 400,
+    color: Colors.UDF,
+    label: I18n('Options')
+  },
+  Files: {
+    categoryId: CategoryId.Files,
+    weight: 300,
+    color: Colors.Files,
+    label: I18n('Files')
+  },
+  VirtualColumn: {
+    categoryId: CategoryId.VirtualColumn,
+    weight: 200,
+    color: Colors.Column,
+    label: I18n('Columns')
+  },
+  ColRefKeyword: {
+    categoryId: CategoryId.ColRefKeyword,
+    weight: 100,
+    color: Colors.Keyword,
+    label: I18n('Keywords')
+  },
+  Variable: {
+    categoryId: CategoryId.Variable,
+    weight: 50,
+    color: Colors.IdentCteVar,
+    label: I18n('Variables')
+  },
+  Keyword: {
+    categoryId: CategoryId.Keyword,
+    weight: 0,
+    color: Colors.Keyword,
+    label: I18n('Keywords')
+  },
+  PopularJoin: {
+    categoryId: CategoryId.PopularJoin,
+    weight: 1500,
+    color: Colors.Popular,
+    label: I18n('Popular')
+  }
+};
+
+export const extractCategories = (suggestions: Suggestion[]): CategoryInfo[] => {
+  const uniqueCategories = new Set<CategoryInfo>();
+  suggestions.forEach(suggestion => {
+    if (suggestion.popular) {
+      uniqueCategories.add(Category.Popular);
+    } else if (
+      suggestion.category.categoryId === CategoryId.Table ||
+      suggestion.category.categoryId === CategoryId.Column ||
+      suggestion.category.categoryId === CategoryId.UDF
+    ) {
+      uniqueCategories.add(Category[suggestion.category.categoryId]);
+    }
+  });
+  const categories: CategoryInfo[] = [...uniqueCategories];
+  categories.sort((a, b) => a.label.localeCompare(b.label));
+  categories.unshift(Category.All);
+  return categories;
+};

+ 34 - 5
desktop/core/src/desktop/js/catalog/dataCatalog/index.d.ts

@@ -18,11 +18,9 @@ import CancellableJqPromise from 'api/cancellableJqPromise';
 import DataCatalogEntry from 'catalog/dataCatalogEntry';
 import DataCatalogEntry from 'catalog/dataCatalogEntry';
 import { Compute, Connector, Namespace } from 'types/config';
 import { Compute, Connector, Namespace } from 'types/config';
 
 
-interface options {
+interface BaseOptions {
   namespace: Namespace;
   namespace: Namespace;
   compute: Compute;
   compute: Compute;
-  connector: Connector;
-  path: string | string[];
   definition?: unknown;
   definition?: unknown;
   silenceErrors?: boolean;
   silenceErrors?: boolean;
   cachedOnly?: boolean;
   cachedOnly?: boolean;
@@ -31,9 +29,40 @@ interface options {
   temporaryOnly?: boolean;
   temporaryOnly?: boolean;
 }
 }
 
 
+interface SingleEntryOptions extends BaseOptions {
+  path: string | string[];
+}
+
+interface MultiEntryOptions extends BaseOptions {
+  paths: (string | string[])[];
+}
+
+interface StaticSingleEntryOptions extends SingleEntryOptions {
+  connector: Connector;
+}
+
+interface StaticMultiEntryOptions extends MultiEntryOptions {
+  connector: Connector;
+}
+
+interface DataCatalog {
+  getChildren(options: SingleEntryOptions): CancellableJqPromise<DataCatalogEntry[]>;
+  getEntry(options: SingleEntryOptions): CancellableJqPromise<DataCatalogEntry>;
+  getMultiTableEntry(options: MultiEntryOptions): CancellableJqPromise<DataCatalogEntry>;
+  loadOptimizerPopularityForTables(
+    options: MultiEntryOptions
+  ): CancellableJqPromise<DataCatalogEntry[]>;
+}
+
 declare const dataCatalog: {
 declare const dataCatalog: {
-  getChildren(options: options): CancellableJqPromise<DataCatalogEntry[]>;
-  getEntry(options: options): CancellableJqPromise<DataCatalogEntry>;
+  getChildren(options: StaticSingleEntryOptions): CancellableJqPromise<DataCatalogEntry[]>;
+  getEntry(options: StaticSingleEntryOptions): CancellableJqPromise<DataCatalogEntry>;
+  getMultiTableEntry(options: StaticMultiEntryOptions): CancellableJqPromise<DataCatalogEntry>;
+  loadOptimizerPopularityForTables(
+    options: StaticMultiEntryOptions
+  ): CancellableJqPromise<DataCatalogEntry[]>;
+  getCatalog(connector: Connector): DataCatalog;
+  applyCancellable(promise: CancellableJqPromise<unknown>, options: { cancellable: boolean }): void;
 };
 };
 
 
 export = dataCatalog;
 export = dataCatalog;

+ 9 - 4
desktop/core/src/desktop/js/sql/sqlUtils.ts

@@ -21,7 +21,10 @@ import CancellableJqPromise from 'api/cancellableJqPromise';
 import dataCatalog from 'catalog/dataCatalog';
 import dataCatalog from 'catalog/dataCatalog';
 import { IdentifierChainEntry, ParsedLocation, ParsedTable } from 'parse/types';
 import { IdentifierChainEntry, ParsedLocation, ParsedTable } from 'parse/types';
 import { isReserved } from 'sql/reference/sqlReferenceRepository';
 import { isReserved } from 'sql/reference/sqlReferenceRepository';
-import { Suggestion } from 'apps/notebook2/components/aceEditor/autocomplete/AutocompleteResults';
+import {
+  CommentDetails,
+  Suggestion
+} from 'apps/notebook2/components/aceEditor/autocomplete/AutocompleteResults';
 import { Compute, Connector, Namespace } from 'types/config';
 import { Compute, Connector, Namespace } from 'types/config';
 
 
 const identifierEquals = (a?: string, b?: string): boolean =>
 const identifierEquals = (a?: string, b?: string): boolean =>
@@ -47,10 +50,12 @@ const autocompleteFilter = (filter: string, entries: Suggestion[]): Suggestion[]
       }
       }
     } else if (
     } else if (
       suggestion.details &&
       suggestion.details &&
-      suggestion.details.comment &&
+      (<CommentDetails>suggestion.details).comment &&
       lowerCaseFilter.indexOf(' ') === -1
       lowerCaseFilter.indexOf(' ') === -1
     ) {
     ) {
-      foundIndex = suggestion.details.comment.toLowerCase().indexOf(lowerCaseFilter);
+      foundIndex = (<CommentDetails>suggestion.details).comment
+        .toLowerCase()
+        .indexOf(lowerCaseFilter);
       if (foundIndex !== -1) {
       if (foundIndex !== -1) {
         suggestion.filterWeight = 1;
         suggestion.filterWeight = 1;
         suggestion.matchComment = true;
         suggestion.matchComment = true;
@@ -72,7 +77,7 @@ export interface SortOverride {
 const sortSuggestions = (
 const sortSuggestions = (
   suggestions: Suggestion[],
   suggestions: Suggestion[],
   filter: string,
   filter: string,
-  sortOverride?: SortOverride
+  sortOverride?: SortOverride | null
 ): void => {
 ): void => {
   suggestions.sort((a, b) => {
   suggestions.sort((a, b) => {
     if (filter) {
     if (filter) {