From 79cede1f10178f5bc10286fe0410e3bbb9beaa15 Mon Sep 17 00:00:00 2001
From: Tom Robinson <tlrobinson@gmail.com>
Date: Tue, 17 Mar 2020 22:45:19 -0700
Subject: [PATCH] Allow . separator in expression identifiers (#12148)

* Allow . separator in expression identifiers

* Make fk separator configurable
---
 .../src/metabase/lib/expressions/config.js    |  5 +++
 .../src/metabase/lib/expressions/index.js     | 22 ++++++++--
 .../src/metabase/lib/expressions/lexer.js     |  2 +-
 .../src/metabase/lib/expressions/suggest.js   | 42 ++++++++++++-------
 4 files changed, 50 insertions(+), 21 deletions(-)

diff --git a/frontend/src/metabase/lib/expressions/config.js b/frontend/src/metabase/lib/expressions/config.js
index d23f2dcd67b..60d780106e9 100644
--- a/frontend/src/metabase/lib/expressions/config.js
+++ b/frontend/src/metabase/lib/expressions/config.js
@@ -29,6 +29,11 @@ export const EDITOR_QUOTES = {
 //   identifierAlwaysQuoted: false,
 // };
 
+export const EDITOR_FK_SYMBOLS = {
+  symbols: [".", " → "],
+  default: " → ",
+};
+
 // copied relevant parts from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence
 export const OPERATOR_PRECEDENCE = {
   not: 17,
diff --git a/frontend/src/metabase/lib/expressions/index.js b/frontend/src/metabase/lib/expressions/index.js
index 0b06efe8cdb..0bfb4172587 100644
--- a/frontend/src/metabase/lib/expressions/index.js
+++ b/frontend/src/metabase/lib/expressions/index.js
@@ -1,7 +1,14 @@
 export * from "./config";
 
 import Dimension from "metabase-lib/lib/Dimension";
-import { OPERATORS, FUNCTIONS, EDITOR_QUOTES, getMBQLName } from "./config";
+import { FK_SYMBOL } from "metabase/lib/formatting";
+import {
+  OPERATORS,
+  FUNCTIONS,
+  EDITOR_QUOTES,
+  EDITOR_FK_SYMBOLS,
+  getMBQLName,
+} from "./config";
 
 // IDENTIFIERS
 
@@ -60,15 +67,22 @@ export function parseDimension(name, { query }) {
   return query
     .dimensionOptions()
     .all()
-    .find(d => getDimensionName(d) === name);
+    .find(d =>
+      EDITOR_FK_SYMBOLS.symbols.some(
+        separator => getDimensionName(d, separator) === name,
+      ),
+    );
 }
 
 export function formatDimensionName(dimension, options) {
   return formatIdentifier(getDimensionName(dimension), options);
 }
 
-export function getDimensionName(dimension) {
-  return dimension.render();
+export function getDimensionName(
+  dimension,
+  separator = EDITOR_FK_SYMBOLS.default,
+) {
+  return dimension.render().replace(` ${FK_SYMBOL} `, separator);
 }
 
 // STRING LITERALS
diff --git a/frontend/src/metabase/lib/expressions/lexer.js b/frontend/src/metabase/lib/expressions/lexer.js
index 6e56e7976ae..40bc0fd96ba 100644
--- a/frontend/src/metabase/lib/expressions/lexer.js
+++ b/frontend/src/metabase/lib/expressions/lexer.js
@@ -37,7 +37,7 @@ function createClauseToken(name, options = {}) {
 
 export const Identifier = createToken({
   name: "Identifier",
-  pattern: /\w+/,
+  pattern: /(\w|\.)+/,
   label: "identfier",
 });
 export const IdentifierString = createToken({
diff --git a/frontend/src/metabase/lib/expressions/suggest.js b/frontend/src/metabase/lib/expressions/suggest.js
index 1c1d4c22715..5183be6b401 100644
--- a/frontend/src/metabase/lib/expressions/suggest.js
+++ b/frontend/src/metabase/lib/expressions/suggest.js
@@ -52,6 +52,7 @@ import {
   isExpressionType,
   getFunctionArgType,
   EXPRESSION_TYPES,
+  EDITOR_FK_SYMBOLS,
 } from "./config";
 
 const FUNCTIONS_BY_TYPE = {};
@@ -91,13 +92,23 @@ export function suggest({
   if (
     lastInputToken &&
     ((isTokenType(lastInputToken.tokenType, Identifier) &&
-      /\w/.test(partialSource[partialSource.length - 1])) ||
+      Identifier.PATTERN.test(partialSource[partialSource.length - 1])) ||
       lastInputTokenIsUnclosedIdentifierString)
   ) {
     tokenVector = tokenVector.slice(0, -1);
     partialSuggestionMode = true;
   }
 
+  const identifierTrimOptions = lastInputTokenIsUnclosedIdentifierString
+    ? {
+        // use the last token's pattern anchored to the end of the text
+        prefixTrim: new RegExp(lastInputToken.tokenType.PATTERN.source + "$"),
+      }
+    : {
+        prefixTrim: new RegExp(Identifier.PATTERN.source + "$"),
+        postfixTrim: new RegExp("^" + Identifier.PATTERN.source + "\\s*"),
+      };
+
   const context = getContext({
     cst,
     tokenVector,
@@ -136,15 +147,6 @@ export function suggest({
         parentRule === "metricExpression" &&
         isExpressionType(expectedType, "aggregation");
 
-      const trimOptions = lastInputTokenIsUnclosedIdentifierString
-        ? {
-            // use the last token's pattern anchored to the end of the text
-            prefixTrim: new RegExp(
-              lastInputToken.tokenType.PATTERN.source + "$",
-            ),
-          }
-        : { prefixTrim: /\w+$/, postfixTrim: /^\w+\s*/ };
-
       if (isDimension) {
         let dimensions = [];
         if (
@@ -178,7 +180,10 @@ export function suggest({
             type: "fields",
             name: getDimensionName(dimension),
             text: formatDimensionName(dimension) + " ",
-            ...trimOptions,
+            alternates: EDITOR_FK_SYMBOLS.symbols.map(symbol =>
+              getDimensionName(dimension, symbol),
+            ),
+            ...identifierTrimOptions,
           })),
         );
       }
@@ -188,7 +193,7 @@ export function suggest({
             type: "segments",
             name: segment.name,
             text: formatSegmentName(segment),
-            ...trimOptions,
+            ...identifierTrimOptions,
           })),
         );
       }
@@ -198,7 +203,7 @@ export function suggest({
             type: "metrics",
             name: metric.name,
             text: formatMetricName(metric),
-            ...trimOptions,
+            ...identifierTrimOptions,
           })),
         );
       }
@@ -315,11 +320,16 @@ export function suggest({
 
   // throw away any suggestion that is not a suffix of the last partialToken.
   if (partialSuggestionMode) {
+    const input = lastInputToken.image;
     const partial = lastInputTokenIsUnclosedIdentifierString
-      ? lastInputToken.image.slice(1).toLowerCase()
-      : lastInputToken.image.toLowerCase();
+      ? input.slice(1).toLowerCase()
+      : input.toLowerCase();
     for (const suggestion of finalSuggestions) {
-      suggestion: for (const text of [suggestion.name, suggestion.text]) {
+      suggestion: for (const text of [
+        suggestion.name,
+        suggestion.text,
+        ...(suggestion.alternates || []),
+      ]) {
         const lower = (text || "").toLowerCase();
         if (lower.startsWith(partial)) {
           suggestion.range = [0, partial.length];
-- 
GitLab