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