diff --git a/frontend/src/metabase/lib/expressions/config.js b/frontend/src/metabase/lib/expressions/config.js index d23f2dcd67bf064d88b6b40ea9d34b73b4123bdf..60d780106e9b4c8b377767bca09e63738277f2ea 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 0b06efe8cdb8bccbec4374fbb322c920b652a8b6..0bfb41725879237f0b610925fa5b3e2286fe0b1f 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 6e56e7976ae9b870bb5dcca90b9bd65d26f41939..40bc0fd96ba8b1ea0c3aa53ffc0b489fdfe1590a 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 1c1d4c2271504f5d5ce7af530543ab6255bdd76a..5183be6b40173cccbbb1f2557e6337f011e627ae 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];