diff --git a/redisinsight/ui/src/pages/search/utils/query.ts b/redisinsight/ui/src/pages/search/utils/query.ts index a18498d5d5..b3e0f984f6 100644 --- a/redisinsight/ui/src/pages/search/utils/query.ts +++ b/redisinsight/ui/src/pages/search/utils/query.ts @@ -457,8 +457,8 @@ export const findArgByToken = (list: SearchCommand[], arg: string): Maybe oneOfArg?.token?.toLowerCase() === arg?.toLowerCase()) : cArg.arguments?.[0]?.token?.toLowerCase() === arg.toLowerCase())) -export const isCompositeArgument = (arg: string, prevArg?: string) => - COMPOSITE_ARGS.includes([prevArg?.toUpperCase(), arg?.toUpperCase()].join(' ')) +export const isCompositeArgument = (arg: string, prevArg?: string, args: string[] = []) => + args.includes([prevArg?.toUpperCase(), arg?.toUpperCase()].join(' ')) export const generateDetail = (command: Maybe) => { if (!command) return '' diff --git a/redisinsight/ui/src/pages/workbench/components/query/Query/Query.tsx b/redisinsight/ui/src/pages/workbench/components/query/Query/Query.tsx index 7670e4b8f2..e9fdbce023 100644 --- a/redisinsight/ui/src/pages/workbench/components/query/Query/Query.tsx +++ b/redisinsight/ui/src/pages/workbench/components/query/Query/Query.tsx @@ -5,17 +5,11 @@ import cx from 'classnames' import MonacoEditor, { monaco as monacoEditor } from 'react-monaco-editor' import { useParams } from 'react-router-dom' -import { - Theme, - MonacoLanguage, - DSLNaming, - IRedisCommand, -} from 'uiSrc/constants' +import { DSLNaming, ICommandTokenType, IRedisCommand, MonacoLanguage, Theme, } from 'uiSrc/constants' import { actionTriggerParameterHints, createSyntaxWidget, decoration, - findArgIndexByCursor, findCompleteQuery, getMonacoAction, IMonacoQuery, @@ -28,35 +22,26 @@ import { ThemeContext } from 'uiSrc/contexts/themeContext' import { appRedisCommandsSelector } from 'uiSrc/slices/app/redis-commands' import { IEditorMount, ISnippetController } from 'uiSrc/pages/workbench/interfaces' import { CommandExecutionUI, RedisResponseBuffer } from 'uiSrc/slices/interfaces' -import { RunQueryMode, ResultsMode } from 'uiSrc/slices/interfaces/workbench' +import { ResultsMode, RunQueryMode } from 'uiSrc/slices/interfaces/workbench' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import { stopProcessing, workbenchResultsSelector } from 'uiSrc/slices/workbench/wb-results' import DedicatedEditor from 'uiSrc/components/monaco-editor/components/dedicated-editor' import { QueryActions, QueryTutorials } from 'uiSrc/components/query' -import { - addOwnTokenToArgs, -} from 'uiSrc/pages/workbench/utils/query' +import { addOwnTokenToArgs, findCurrentArgument, } from 'uiSrc/pages/workbench/utils/query' import { getRange, getRediSearchSignutureProvider, } from 'uiSrc/pages/workbench/utils/monaco' import { CursorContext } from 'uiSrc/pages/workbench/types' -import { - asSuggestionsRef, - getCommandsSuggestions, - isIndexComplete -} from 'uiSrc/pages/workbench/utils/suggestions' -import { - COMMANDS_TO_GET_INDEX_INFO, - EmptySuggestionsIds, -} from 'uiSrc/pages/workbench/constants' +import { asSuggestionsRef, getCommandsSuggestions, isIndexComplete } from 'uiSrc/pages/workbench/utils/suggestions' +import { COMMANDS_TO_GET_INDEX_INFO, COMPOSITE_ARGS, EmptySuggestionsIds, } from 'uiSrc/pages/workbench/constants' import { useDebouncedEffect } from 'uiSrc/services' import { fetchRedisearchInfoAction } from 'uiSrc/slices/browser/redisearch' import { findSuggestionsByArg } from 'uiSrc/pages/workbench/utils/searchSuggestions' import { - aroundQuotesRegExp, argInQuotesRegExp, + aroundQuotesRegExp, + options, SYNTAX_CONTEXT_ID, SYNTAX_WIDGET_ID, - options, TUTORIALS } from './constants' import styles from './styles.module.scss' @@ -343,7 +328,7 @@ const Query = (props: Props) => { return } - const command = findCompleteQuery(model, e.position, REDIS_COMMANDS_SPEC, REDIS_COMMANDS_ARRAY) + const command = findCompleteQuery(model, e.position, REDIS_COMMANDS_SPEC, REDIS_COMMANDS_ARRAY, COMPOSITE_ARGS) handleSuggestions(editor, command) handleDslSyntax(e, command) } @@ -427,33 +412,30 @@ const Query = (props: Props) => { return } - const queryArgIndex = command.info?.arguments?.findIndex((arg) => arg.dsl) || -1 - const cursorPosition = command.commandCursorPosition || 0 - const { allArgs } = command || {} - if (!allArgs.length || queryArgIndex < 0) { + const isContainsDSL = command.info?.arguments?.some((arg) => arg.dsl) + if (!isContainsDSL) { isWidgetEscaped.current = false return } - const argIndex = findArgIndexByCursor(allArgs, command.fullQuery, cursorPosition) - if (argIndex === null) { - isWidgetEscaped.current = false - return - } - - const queryArg = allArgs[argIndex] - const argDSL = command.info?.arguments?.[argIndex]?.dsl || '' + const [beforeOffsetArgs, [currentOffsetArg]] = command.args + const foundArg = findCurrentArgument([{ + ...command.info, + type: ICommandTokenType.Block, + token: command.name, + arguments: command.info?.arguments + }], beforeOffsetArgs) - if (queryArgIndex === argIndex && argInQuotesRegExp.test(queryArg)) { + const DSL = foundArg?.stopArg?.dsl + if (DSL && argInQuotesRegExp.test(currentOffsetArg)) { if (isWidgetEscaped.current) return - const lang = DSLNaming[argDSL] ?? null + + const lang = DSLNaming[DSL] ?? null lang && showSyntaxWidget(editor, e.position, lang) - selectedArg.current = queryArg - syntaxCommand.current = { - ...command, - lang: argDSL, - argToReplace: queryArg - } + selectedArg.current = currentOffsetArg + syntaxCommand.current = { ...command, lang: DSL, argToReplace: currentOffsetArg } + } else { + isWidgetEscaped.current = false } } diff --git a/redisinsight/ui/src/pages/workbench/data/supported_commands.json b/redisinsight/ui/src/pages/workbench/data/supported_commands.json index fa08d11556..6765dceb75 100644 --- a/redisinsight/ui/src/pages/workbench/data/supported_commands.json +++ b/redisinsight/ui/src/pages/workbench/data/supported_commands.json @@ -714,14 +714,10 @@ "optional": true }, { - "name": "queryword", - "type": "pure-token", + "name": "query", + "type": "token", "token": "QUERY", "expression": true - }, - { - "name": "query", - "type": "string" } ], "since": "2.2.0", diff --git a/redisinsight/ui/src/pages/workbench/utils/query.ts b/redisinsight/ui/src/pages/workbench/utils/query.ts index 3bda2a3bc0..9120c3d876 100644 --- a/redisinsight/ui/src/pages/workbench/utils/query.ts +++ b/redisinsight/ui/src/pages/workbench/utils/query.ts @@ -1,106 +1,10 @@ /* eslint-disable no-continue */ -import { isNumber, toNumber } from 'lodash' +import { findLastIndex, isNumber, toNumber } from 'lodash' import { generateArgsNames, Maybe, Nullable } from 'uiSrc/utils' import { CommandProvider, IRedisCommand, IRedisCommandTree, ICommandTokenType } from 'uiSrc/constants' -import { COMPOSITE_ARGS } from 'uiSrc/pages/workbench/constants' import { ArgName, FoundCommandArgument } from '../types' -export const splitQueryByArgs = (query: string, position: number = 0) => { - const args: [string[], string[]] = [[], []] - let arg = '' - let inQuotes = false - let escapeNextChar = false - let quoteChar = '' - let isCursorInQuotes = false - let lastArg = '' - let argLeftOffset = 0 - let argRightOffset = 0 - - const pushToProperTuple = (isAfterOffset: boolean, arg: string) => { - lastArg = arg - isAfterOffset ? args[1].push(arg) : args[0].push(arg) - } - - const updateLastArgument = (isAfterOffset: boolean, arg: string) => { - const argsBySide = args[isAfterOffset ? 1 : 0] - argsBySide[argsBySide.length - 1] = `${argsBySide[argsBySide.length - 1]} ${arg}` - } - - const updateArgOffsets = (left: number, right: number) => { - argLeftOffset = left - argRightOffset = right - } - - for (let i = 0; i < query.length; i++) { - const char = query[i] - const isAfterOffset = i >= position + (inQuotes ? -1 : 0) - - if (escapeNextChar) { - arg += char - escapeNextChar = !quoteChar - } else if (char === '\\') { - escapeNextChar = true - } else if (inQuotes) { - if (char === quoteChar) { - inQuotes = false - const argWithChat = arg + char - - if (isAfterOffset && !argLeftOffset) { - updateArgOffsets(i - arg.length, i + 1) - } - - if (isCompositeArgument(argWithChat, lastArg)) { - updateLastArgument(isAfterOffset, argWithChat) - } else { - pushToProperTuple(isAfterOffset, argWithChat) - } - - arg = '' - } else { - arg += char - } - } else if (char === '"' || char === "'") { - inQuotes = true - quoteChar = char - arg += char - } else if (char === ' ' || char === '\n') { - if (arg.length > 0) { - if (isAfterOffset && !argLeftOffset) { - updateArgOffsets(i - arg.length, i) - } - - if (isCompositeArgument(arg, lastArg)) { - updateLastArgument(isAfterOffset, arg) - } else { - pushToProperTuple(isAfterOffset, arg) - } - - arg = '' - } - } else { - arg += char - } - - if (i === position - 1) isCursorInQuotes = inQuotes - } - - if (arg.length > 0) { - if (!argLeftOffset) updateArgOffsets(query.length - arg.length, query.length) - pushToProperTuple(true, arg) - } - - const cursor = { - isCursorInQuotes, - prevCursorChar: query[position - 1]?.trim() || '', - nextCursorChar: query[position]?.trim() || '', - argLeftOffset, - argRightOffset - } - - return { args, cursor } -} - export const findCurrentArgument = ( args: IRedisCommand[], prev: string[], @@ -340,7 +244,7 @@ export const getArgumentSuggestions = ( const isBlockHasParent = current?.arguments?.some(({ name }) => parent?.name && name === parent?.name) const foundParent = isBlockHasParent ? { ...parent, parent: current } : (parent || current) - const isBlockComplete = !stopArgument && current?.name === lastArgument?.name + const isBlockComplete = !stopArgument && isPrevArgWasMandatory const beforeMandatoryOptionalArgs = getAllRestArguments(foundParent, lastArgument, untilTokenArgs, isBlockComplete) const requiredArgsLength = restNotFilledArgs.filter((arg) => !arg.optional).length @@ -395,8 +299,14 @@ export const getAllRestArguments = ( skipLevel = false ) => { const appendArgs: Array = [] - const currentLvlNextArgs = removeNotSuggestedArgs( + + const currentToken = current?.type === ICommandTokenType.Block ? current?.arguments?.[0].token : current?.token + const lastTokenIndex = findLastIndex( untilTokenArgs, + (arg) => arg?.toLowerCase() === currentToken?.toLowerCase() + ) + const currentLvlNextArgs = removeNotSuggestedArgs( + untilTokenArgs.slice(lastTokenIndex > 0 ? lastTokenIndex : 0), getRestArguments(current, stopArgument) ) @@ -464,10 +374,7 @@ export const findArgByToken = (list: IRedisCommand[], arg: string): Maybe (cArg.type === ICommandTokenType.OneOf ? cArg.arguments?.some((oneOfArg: IRedisCommand) => oneOfArg?.token?.toLowerCase() === arg?.toLowerCase()) - : cArg.arguments?.[0]?.token?.toLowerCase() === arg.toLowerCase())) - -export const isCompositeArgument = (arg: string, prevArg?: string) => - COMPOSITE_ARGS.includes([prevArg?.toUpperCase(), arg?.toUpperCase()].join(' ')) + : cArg.arguments?.[0]?.token?.toLowerCase() === arg?.toLowerCase())) export const generateDetail = (command: Maybe) => { if (!command) return '' diff --git a/redisinsight/ui/src/pages/workbench/utils/searchSuggestions.ts b/redisinsight/ui/src/pages/workbench/utils/searchSuggestions.ts index 12c8e314db..3ca7542f9a 100644 --- a/redisinsight/ui/src/pages/workbench/utils/searchSuggestions.ts +++ b/redisinsight/ui/src/pages/workbench/utils/searchSuggestions.ts @@ -1,8 +1,8 @@ import { monaco as monacoEditor } from 'react-monaco-editor' import { isNumber } from 'lodash' -import { IMonacoQuery, Nullable } from 'uiSrc/utils' +import { IMonacoQuery, Nullable, splitQueryByArgs } from 'uiSrc/utils' import { CursorContext, FoundCommandArgument } from 'uiSrc/pages/workbench/types' -import { findCurrentArgument, splitQueryByArgs } from 'uiSrc/pages/workbench/utils/query' +import { findCurrentArgument } from 'uiSrc/pages/workbench/utils/query' import { IRedisCommand } from 'uiSrc/constants' import { asSuggestionsRef, @@ -49,28 +49,27 @@ export const findSuggestionsByArg = ( return handleFieldSuggestions(additionData.fields || [], foundArg, cursorContext.range) } + if (foundArg?.stopArg?.token && !foundArg?.isBlocked) { + return handleCommonSuggestions( + command.fullQuery, + foundArg, + allArgs, + additionData.fields || [], + cursorContext, + isEscaped + ) + } + + const { indexes, fields } = additionData switch (foundArg?.stopArg?.name) { case DefinedArgumentName.index: { - return handleIndexSuggestions( - additionData.indexes || [], - command, - foundArg, - currentOffsetArg, - cursorContext.range - ) + return handleIndexSuggestions(indexes, command, foundArg, currentOffsetArg, cursorContext) } case DefinedArgumentName.query: { return handleQuerySuggestions(foundArg) } default: { - return handleCommonSuggestions( - command.fullQuery, - foundArg, - allArgs, - additionData.fields || [], - cursorContext, - isEscaped - ) + return handleCommonSuggestions(command.fullQuery, foundArg, allArgs, fields, cursorContext, isEscaped) } } } @@ -88,11 +87,11 @@ const handleFieldSuggestions = ( } const handleIndexSuggestions = ( - indexes: any[], + indexes: any[] = [], command: IMonacoQuery, foundArg: FoundCommandArgument, currentOffsetArg: Nullable, - range: monacoEditor.IRange + cursorContext: CursorContext ) => { const isIndex = indexes.length > 0 const helpWidget = { isOpen: isIndex, parent: command.info, currentArg: foundArg?.stopArg } @@ -109,7 +108,7 @@ const handleIndexSuggestions = ( helpWidget.isOpen = !!currentOffsetArg return { - suggestions: asSuggestionsRef(!currentOffsetArg ? getNoIndexesSuggestion(range) : [], true), + suggestions: asSuggestionsRef(!currentOffsetArg ? getNoIndexesSuggestion(cursorContext.range) : [], true), helpWidget } } @@ -127,7 +126,7 @@ const handleIndexSuggestions = ( && currentCommand?.arguments?.[argumentIndex + 1]?.name === DefinedArgumentName.query return { - suggestions: asSuggestionsRef(getIndexesSuggestions(indexes, range, isNextArgQuery)), + suggestions: asSuggestionsRef(getIndexesSuggestions(indexes, cursorContext.range, isNextArgQuery)), helpWidget } } @@ -171,11 +170,13 @@ const handleCommonSuggestions = ( value: string, foundArg: Nullable, allArgs: string[], - fields: any[], + fields: any[] = [], cursorContext: CursorContext, isEscaped: boolean ) => { - if (foundArg?.stopArg?.expression) return handleExpressionSuggestions(value, foundArg, cursorContext) + if (foundArg?.stopArg?.expression && foundArg.isBlocked) { + return handleExpressionSuggestions(value, foundArg, cursorContext) + } const { prevCursorChar, nextCursorChar, isCursorInQuotes } = cursorContext const shouldHideSuggestions = isCursorInQuotes || nextCursorChar || (prevCursorChar && isEscaped) diff --git a/redisinsight/ui/src/pages/workbench/utils/tests/query.spec.ts b/redisinsight/ui/src/pages/workbench/utils/tests/query.spec.ts index 5afe4fe26b..a3ab9fc6fd 100644 --- a/redisinsight/ui/src/pages/workbench/utils/tests/query.spec.ts +++ b/redisinsight/ui/src/pages/workbench/utils/tests/query.spec.ts @@ -1,13 +1,14 @@ import { SearchCommand, TokenType } from 'uiSrc/pages/search/types' -import { Maybe } from 'uiSrc/utils' +import { Maybe, splitQueryByArgs } from 'uiSrc/utils' import { MOCKED_REDIS_COMMANDS } from 'uiSrc/mocks/data/mocked_redis_commands' import { IRedisCommand } from 'uiSrc/constants' +import { COMPOSITE_ARGS } from 'uiSrc/pages/workbench/constants' import { commonfindCurrentArgumentCases, findArgumentftAggreageTests, findArgumentftSearchTests } from './test-cases' -import { addOwnTokenToArgs, findCurrentArgument, generateDetail, splitQueryByArgs } from '../query' +import { addOwnTokenToArgs, findCurrentArgument, generateDetail } from '../query' const ftSearchCommand = MOCKED_REDIS_COMMANDS['FT.SEARCH'] const ftAggregateCommand = MOCKED_REDIS_COMMANDS['FT.AGGREGATE'] @@ -20,7 +21,7 @@ describe('findCurrentArgument', () => { describe('with list of commands', () => { commonfindCurrentArgumentCases.forEach(({ input, result, appendIncludes, appendNotIncludes }) => { it(`should return proper suggestions for ${input}`, () => { - const { args } = splitQueryByArgs(input) + const { args } = splitQueryByArgs(input, 0, COMPOSITE_ARGS) const COMMANDS_LIST = COMMANDS.map((command) => ({ ...addOwnTokenToArgs(command.name!, command), token: command.name!, @@ -76,84 +77,6 @@ describe('findCurrentArgument', () => { }) }) -const splitQueryByArgsTests: Array<{ - input: [string, number?] - result: any -}> = [ - { - input: ['FT.SEARCH "idx:bicycle" "" WITHSORTKEYS'], - result: { - args: [[], ['FT.SEARCH', '"idx:bicycle"', '""', 'WITHSORTKEYS']], - cursor: { - argLeftOffset: 10, - argRightOffset: 23, - isCursorInQuotes: false, - nextCursorChar: 'F', - prevCursorChar: '' - } - } - }, - { - input: ['FT.SEARCH "idx:bicycle" "" WITHSORTKEYS', 17], - result: { - args: [['FT.SEARCH'], ['"idx:bicycle"', '""', 'WITHSORTKEYS']], - cursor: { - argLeftOffset: 10, - argRightOffset: 23, - isCursorInQuotes: true, - nextCursorChar: 'c', - prevCursorChar: 'i' - } - } - }, - { - input: ['FT.SEARCH "idx:bicycle" "" WITHSORTKEYS', 39], - result: { - args: [['FT.SEARCH', '"idx:bicycle"', '""'], ['WITHSORTKEYS']], - cursor: { - argLeftOffset: 27, - argRightOffset: 39, - isCursorInQuotes: false, - nextCursorChar: '', - prevCursorChar: 'S' - } - } - }, - { - input: ['FT.SEARCH "idx:bicycle" "" WITHSORTKEYS ', 40], - result: { - args: [['FT.SEARCH', '"idx:bicycle"', '""', 'WITHSORTKEYS'], []], - cursor: { - argLeftOffset: 0, - argRightOffset: 0, - isCursorInQuotes: false, - nextCursorChar: '', - prevCursorChar: '' - } - } - }, - { - input: ['FT.SEARCH "idx:bicycle \\" \\"" "" WITHSORTKEYS ', 46], - result: { - args: [['FT.SEARCH', '"idx:bicycle " ""', '""', 'WITHSORTKEYS'], []], - cursor: { - argLeftOffset: 0, - argRightOffset: 0, - isCursorInQuotes: false, - nextCursorChar: '', - prevCursorChar: '' - } - } - } -] - -describe('splitQueryByArgs', () => { - it.each(splitQueryByArgsTests)('should return for %input proper result', ({ input, result }) => { - const testResult = splitQueryByArgs(...input) - expect(testResult).toEqual(result) - }) -}) - const generateDetailTests: Array<{ input: Maybe, result: any }> = [ { input: ftSearchCommand.arguments.find(({ name }) => name === 'nocontent') as SearchCommand, diff --git a/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/common.ts b/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/common.ts index 2f8b3f3a1d..f7a9ff23b8 100644 --- a/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/common.ts +++ b/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/common.ts @@ -24,6 +24,22 @@ export const commonfindCurrentArgumentCases = [ appendIncludes: ['REDUCE', 'APPLY', 'SORTBY', 'GROUPBY'], appendNotIncludes: ['AS'], }, + { + input: 'FT.AGGREGATE \'idx1:vd\' "*" GROUPBY 1 @location REDUCE COUNT 0 AS item_count REDUCE SUM 1 @students ', + result: { + stopArg: { + name: 'name', + optional: true, + token: 'AS', + type: 'string' + }, + append: expect.any(Array), + isBlocked: false, + isComplete: true, + parent: expect.any(Object) + }, + appendIncludes: ['AS', 'REDUCE', 'APPLY', 'SORTBY', 'GROUPBY'], + }, { input: 'FT.SEARCH "idx:bicycle" "*" ', result: { @@ -48,6 +64,18 @@ export const commonfindCurrentArgumentCases = [ appendIncludes: ['LIMITED', 'QUERY'], appendNotIncludes: ['AGGREGATE', 'SEARCH'], }, + { + input: 'FT.PROFILE idx AGGREGATE LIMITED ', + result: expect.any(Object), + appendIncludes: ['QUERY'], + appendNotIncludes: ['LIMITED', 'SEARCH'], + }, + { + input: 'FT.PROFILE \'idx:schools\' SEARCH QUERY \'q\' ', + result: expect.any(Object), + appendIncludes: [], + appendNotIncludes: ['LIMITED'], + }, { input: 'FT.CREATE "idx:schools" ', result: expect.any(Object), diff --git a/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/ft-aggregate.ts b/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/ft-aggregate.ts index e1411809a9..fcee36b2c8 100644 --- a/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/ft-aggregate.ts +++ b/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/ft-aggregate.ts @@ -258,7 +258,7 @@ export const findArgumentftAggreageTests = [ args: ['index', '"query"', 'LOAD', '4', '1', '2', '3', '4'], result: { stopArg: undefined, - append: [[]], + append: [], isBlocked: false, isComplete: true, parent: expect.any(Object) diff --git a/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/ft-search.ts b/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/ft-search.ts index 28137bf8e9..a729a10fa5 100644 --- a/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/ft-search.ts +++ b/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/ft-search.ts @@ -133,9 +133,7 @@ export const findArgumentftSearchTests = [ args: ['', '', 'RETURN', '1', 'iden'], result: { stopArg: undefined, - append: [ - [] - ], + append: [], isBlocked: false, isComplete: true, parent: expect.any(Object) @@ -171,9 +169,7 @@ export const findArgumentftSearchTests = [ args: ['', '', 'RETURN', '2', 'iden', 'iden'], result: { stopArg: undefined, - append: [ - [] - ], + append: [], isBlocked: false, isComplete: true, parent: expect.any(Object) @@ -209,9 +205,7 @@ export const findArgumentftSearchTests = [ args: ['', '', 'RETURN', '3', 'iden', 'iden', 'AS', 'iden2'], result: { stopArg: undefined, - append: [ - [] - ], + append: [], isBlocked: false, isComplete: true, parent: expect.any(Object) @@ -262,7 +256,7 @@ export const findArgumentftSearchTests = [ args: ['', '', 'SORTBY', 'f', 'DESC'], result: { stopArg: undefined, - append: [], + append: [[]], isBlocked: false, isComplete: true, parent: expect.any(Object) diff --git a/redisinsight/ui/src/utils/monaco/monacoUtils.ts b/redisinsight/ui/src/utils/monaco/monacoUtils.ts index 4ff48b893e..db18fedb03 100644 --- a/redisinsight/ui/src/utils/monaco/monacoUtils.ts +++ b/redisinsight/ui/src/utils/monaco/monacoUtils.ts @@ -1,7 +1,7 @@ import { monaco as monacoEditor } from 'react-monaco-editor' import { first, isEmpty, isUndefined, reject, without } from 'lodash' import { decode } from 'html-entities' -import { ICommands, ICommand } from 'uiSrc/constants' +import { ICommand, ICommands } from 'uiSrc/constants' import { generateArgsForInsertText, generateArgsNames, @@ -10,7 +10,6 @@ import { IMonacoQuery } from 'uiSrc/utils' import { TJMESPathFunctions } from 'uiSrc/slices/interfaces' -import { isCompositeArgument } from 'uiSrc/pages/search/utils' import { Nullable } from '../types' import { getCommandRepeat, isRepeatCountCorrect } from '../commands' @@ -97,16 +96,21 @@ export const findCommandEarlier = ( return null } - const command:IMonacoCommand = { + return { position, name: matchedCommand, info: commandsSpec[matchedCommand] } - - return command } -export const splitQueryByArgs = (query: string, position: number = 0) => { +export const isCompositeArgument = (arg: string, prevArg?: string, args: string[] = []) => + args.includes([prevArg?.toUpperCase(), arg?.toUpperCase()].join(' ')) + +export const splitQueryByArgs = ( + query: string, + position: number = 0, + compositeArgs: string[] = [] +) => { const args: [string[], string[]] = [[], []] let arg = '' let inQuotes = false @@ -144,16 +148,16 @@ export const splitQueryByArgs = (query: string, position: number = 0) => { } else if (inQuotes) { if (char === quoteChar) { inQuotes = false - const argWithChat = arg + char + const argWithChar = arg + char if (isAfterOffset && !argLeftOffset) { updateArgOffsets(i - arg.length, i + 1) } - if (isCompositeArgument(argWithChat, lastArg)) { - updateLastArgument(isAfterOffset, argWithChat) + if (isCompositeArgument(argWithChar, lastArg, compositeArgs)) { + updateLastArgument(isAfterOffset, argWithChar) } else { - pushToProperTuple(isAfterOffset, argWithChat) + pushToProperTuple(isAfterOffset, argWithChar) } arg = '' @@ -170,7 +174,7 @@ export const splitQueryByArgs = (query: string, position: number = 0) => { updateArgOffsets(i - arg.length, i) } - if (isCompositeArgument(arg, lastArg)) { + if (isCompositeArgument(arg, lastArg, compositeArgs)) { updateLastArgument(isAfterOffset, arg) } else { pushToProperTuple(isAfterOffset, arg) @@ -205,7 +209,8 @@ export const findCompleteQuery = ( model: monacoEditor.editor.ITextModel, position: monacoEditor.Position, commandsSpec: ICommands = {}, - commandsArray: string[] = [] + commandsArray: string[] = [], + compositeArgs: string[] = [] ): Nullable => { const { lineNumber } = position let commandName = '' @@ -262,7 +267,11 @@ export const findCompleteQuery = ( fullQuery += lineAfterPosition } - const { args, cursor } = splitQueryByArgs(fullQuery, commandCursorPosition) + const { args, cursor } = splitQueryByArgs( + fullQuery, + commandCursorPosition, + compositeArgs, + ) return { position, diff --git a/redisinsight/ui/src/utils/monaco/subTokens/redisearchSubTokens.ts b/redisinsight/ui/src/utils/monaco/subTokens/redisearchSubTokens.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/redisinsight/ui/src/utils/tests/monaco/monacoUtils.spec.ts b/redisinsight/ui/src/utils/tests/monaco/monacoUtils.spec.ts index 89c21ed025..89ddbf8245 100644 --- a/redisinsight/ui/src/utils/tests/monaco/monacoUtils.spec.ts +++ b/redisinsight/ui/src/utils/tests/monaco/monacoUtils.spec.ts @@ -4,7 +4,9 @@ import { splitMonacoValuePerLines, findArgIndexByCursor, isParamsLine, - getMonacoLines, getCommandsFromQuery + getMonacoLines, + getCommandsFromQuery, + splitQueryByArgs } from 'uiSrc/utils' describe('removeMonacoComments', () => { @@ -209,3 +211,81 @@ describe('getCommandsFromQuery', () => { } ) }) + +const splitQueryByArgsTests: Array<{ + input: [string, number?] + result: any +}> = [ + { + input: ['FT.SEARCH "idx:bicycle" "" WITHSORTKEYS'], + result: { + args: [[], ['FT.SEARCH', '"idx:bicycle"', '""', 'WITHSORTKEYS']], + cursor: { + argLeftOffset: 10, + argRightOffset: 23, + isCursorInQuotes: false, + nextCursorChar: 'F', + prevCursorChar: '' + } + } + }, + { + input: ['FT.SEARCH "idx:bicycle" "" WITHSORTKEYS', 17], + result: { + args: [['FT.SEARCH'], ['"idx:bicycle"', '""', 'WITHSORTKEYS']], + cursor: { + argLeftOffset: 10, + argRightOffset: 23, + isCursorInQuotes: true, + nextCursorChar: 'c', + prevCursorChar: 'i' + } + } + }, + { + input: ['FT.SEARCH "idx:bicycle" "" WITHSORTKEYS', 39], + result: { + args: [['FT.SEARCH', '"idx:bicycle"', '""'], ['WITHSORTKEYS']], + cursor: { + argLeftOffset: 27, + argRightOffset: 39, + isCursorInQuotes: false, + nextCursorChar: '', + prevCursorChar: 'S' + } + } + }, + { + input: ['FT.SEARCH "idx:bicycle" "" WITHSORTKEYS ', 40], + result: { + args: [['FT.SEARCH', '"idx:bicycle"', '""', 'WITHSORTKEYS'], []], + cursor: { + argLeftOffset: 0, + argRightOffset: 0, + isCursorInQuotes: false, + nextCursorChar: '', + prevCursorChar: '' + } + } + }, + { + input: ['FT.SEARCH "idx:bicycle \\" \\"" "" WITHSORTKEYS ', 46], + result: { + args: [['FT.SEARCH', '"idx:bicycle " ""', '""', 'WITHSORTKEYS'], []], + cursor: { + argLeftOffset: 0, + argRightOffset: 0, + isCursorInQuotes: false, + nextCursorChar: '', + prevCursorChar: '' + } + } + } +] + +describe('splitQueryByArgs', () => { + it.each(splitQueryByArgsTests)('should return for %input proper result', ({ input, result }) => { + const testResult = splitQueryByArgs(...input) + expect(testResult).toEqual(result) + }) +})