diff --git a/frontend/src/components/QueryBuilderV2/CodeMirrorWhereClause/CodeMirrorWhereClause.tsx b/frontend/src/components/QueryBuilderV2/CodeMirrorWhereClause/CodeMirrorWhereClause.tsx index 9327dd2d984a..f4877b3f9685 100644 --- a/frontend/src/components/QueryBuilderV2/CodeMirrorWhereClause/CodeMirrorWhereClause.tsx +++ b/frontend/src/components/QueryBuilderV2/CodeMirrorWhereClause/CodeMirrorWhereClause.tsx @@ -14,17 +14,73 @@ import { CompletionContext, CompletionResult, } from '@codemirror/autocomplete'; -import CodeMirror, { EditorView } from '@uiw/react-codemirror'; +import CodeMirror, { EditorView, Extension } from '@uiw/react-codemirror'; import { Badge, Card, Divider, Space, Tooltip, Typography } from 'antd'; import { useGetQueryKeySuggestions } from 'hooks/querySuggestions/useGetQueryKeySuggestions'; // import { useGetQueryKeyValueSuggestions } from 'hooks/querySuggestions/useGetQueryKeyValueSuggestions'; import { useCallback, useEffect, useRef, useState } from 'react'; import { IQueryContext, IValidationResult } from 'types/antlrQueryTypes'; import { QueryKeySuggestionsProps } from 'types/api/querySuggestions/types'; -import { getQueryContextAtCursor, validateQuery } from 'utils/antlrQueryUtils'; +import { + getQueryContextAtCursor, + queryOperatorSuggestions, + validateQuery, +} from 'utils/antlrQueryUtils'; const { Text, Title } = Typography; +function collapseSpacesOutsideStrings(): Extension { + return EditorView.inputHandler.of((view, from, to, text) => { + // Get the current line text + const { state } = view; + const line = state.doc.lineAt(from); + + // Find the position within the line + const before = line.text.slice(0, from - line.from); + const after = line.text.slice(to - line.from); + + const fullText = before + text + after; + + let insideString = false; + let escaped = false; + let processed = ''; + + for (let i = 0; i < fullText.length; i++) { + const char = fullText[i]; + + if (char === '"' && !escaped) { + insideString = !insideString; + } + if (char === '\\' && !escaped) { + escaped = true; + } else { + escaped = false; + } + + if (!insideString && char === ' ' && processed.endsWith(' ')) { + // Collapse multiple spaces outside strings + // Skip this space + } else { + processed += char; + } + } + + // Only dispatch if the processed text differs + if (processed !== fullText) { + view.dispatch({ + changes: { + from: line.from, + to: line.to, + insert: processed, + }, + }); + return true; + } + + return false; + }); +} + function CodeMirrorWhereClause(): JSX.Element { const [query, setQuery] = useState(''); const [isLoading, setIsLoading] = useState(false); @@ -164,11 +220,13 @@ function CodeMirrorWhereClause(): JSX.Element { } else if (queryContext.isInConjunction) { color = 'magenta'; text = 'Conjunction'; - } else if (queryContext.isInParenthesis) { - color = 'grey'; - text = 'Parenthesis'; } + // else if (queryContext.isInParenthesis) { + // color = 'grey'; + // text = 'Parenthesis'; + // } + return ( ', type: 'operator', info: 'Greater than' }, - { label: '<', type: 'operator', info: 'Less than' }, - { label: '>=', type: 'operator', info: 'Greater than or equal to' }, - { label: '<=', type: 'operator', info: 'Less than or equal to' }, - { label: 'LIKE', type: 'operator', info: 'Like' }, - { label: 'ILIKE', type: 'operator', info: 'Case insensitive like' }, - { label: 'BETWEEN', type: 'operator', info: 'Between' }, - { label: 'EXISTS', type: 'operator', info: 'Exists' }, - { label: 'REGEXP', type: 'operator', info: 'Regular expression' }, - { label: 'CONTAINS', type: 'operator', info: 'Contains' }, - { label: 'IN', type: 'operator', info: 'In' }, - { label: 'NOT', type: 'operator', info: 'Not' }, - // Add more operator options here - ]; + options = queryOperatorSuggestions; } else if (queryContext.isInValue) { // refetchQueryKeyValuesSuggestions(); @@ -248,11 +290,6 @@ function CodeMirrorWhereClause(): JSX.Element { { label: 'AND', type: 'conjunction' }, { label: 'OR', type: 'conjunction' }, ]; - } else if (queryContext.isInParenthesis) { - options = [ - { label: '(', type: 'parenthesis' }, - { label: ')', type: 'parenthesis' }, - ]; } return { @@ -279,7 +316,10 @@ function CodeMirrorWhereClause(): JSX.Element { onUpdate={handleUpdate} autoFocus placeholder="Enter your query (e.g., status = 'error' AND service = 'frontend')" - extensions={[autocompletion({ override: [myCompletions] })]} + extensions={[ + autocompletion({ override: [myCompletions] }), + collapseSpacesOutsideStrings(), + ]} /> diff --git a/frontend/src/utils/antlrQueryUtils.ts b/frontend/src/utils/antlrQueryUtils.ts index d1b39a8eeccf..e775c8b518eb 100644 --- a/frontend/src/utils/antlrQueryUtils.ts +++ b/frontend/src/utils/antlrQueryUtils.ts @@ -290,11 +290,11 @@ export function getQueryContextAtCursor( currentToken.type, ); - // Determine if the current token is a parenthesis - const isInParenthesis = [ - FilterQueryLexer.LPAREN, - FilterQueryLexer.RPAREN, - ].includes(currentToken.type); + // // Determine if the current token is a parenthesis + // const isInParenthesis = [ + // FilterQueryLexer.LPAREN, + // FilterQueryLexer.RPAREN, + // ].includes(currentToken.type); // Determine the context based on the token type const isInValue = [ @@ -332,82 +332,82 @@ export function getQueryContextAtCursor( FilterQueryLexer.HASNONE, ].includes(currentToken.type); - // Handle transitions based on spaces and current state - if (isInKey && query[currentToken.stop + 1] === ' ') { - return { - tokenType: currentToken.type, - text: currentToken.text, - start: currentToken.start, - stop: currentToken.stop, - currentToken: currentToken.text, - isInValue: false, - isInKey: false, - isInOperator: true, - isInFunction: false, - isInConjunction: false, - isInParenthesis: false, - }; - } - if (isInOperator && query[currentToken.stop + 1] === ' ') { - return { - tokenType: currentToken.type, - text: currentToken.text, - start: currentToken.start, - stop: currentToken.stop, - currentToken: currentToken.text, - isInValue: true, - isInKey: false, - isInOperator: false, - isInFunction: false, - isInConjunction: false, - isInParenthesis: false, - }; - } - if (isInValue && query[currentToken.stop + 1] === ' ') { - return { - tokenType: currentToken.type, - text: currentToken.text, - start: currentToken.start, - stop: currentToken.stop, - currentToken: currentToken.text, - isInValue: false, - isInKey: false, - isInOperator: false, - isInFunction: false, - isInConjunction: true, - isInParenthesis: false, - }; - } - if (isInConjunction && query[currentToken.stop + 1] === ' ') { - return { - tokenType: currentToken.type, - text: currentToken.text, - start: currentToken.start, - stop: currentToken.stop, - currentToken: currentToken.text, - isInValue: false, - isInKey: true, - isInOperator: false, - isInFunction: false, - isInConjunction: false, - isInParenthesis: false, - }; - } - if (isInParenthesis && query[currentToken.stop + 1] === ' ') { - return { - tokenType: currentToken.type, - text: currentToken.text, - start: currentToken.start, - stop: currentToken.stop, - currentToken: currentToken.text, - isInValue: false, - isInKey: false, // Suggest keys - isInOperator: false, - isInFunction: false, - isInConjunction: true, // Suggest conjunctions - isInParenthesis: false, - }; - } + // // Handle transitions based on spaces and current state + // if (isInKey && query[currentToken.stop + 1] === ' ') { + // return { + // tokenType: currentToken.type, + // text: currentToken.text, + // start: currentToken.start, + // stop: currentToken.stop, + // currentToken: currentToken.text, + // isInValue: false, + // isInKey: false, + // isInOperator: true, + // isInFunction: false, + // isInConjunction: false, + // isInParenthesis: false, + // }; + // } + // if (isInOperator && query[currentToken.stop + 1] === ' ') { + // return { + // tokenType: currentToken.type, + // text: currentToken.text, + // start: currentToken.start, + // stop: currentToken.stop, + // currentToken: currentToken.text, + // isInValue: true, + // isInKey: false, + // isInOperator: false, + // isInFunction: false, + // isInConjunction: false, + // isInParenthesis: false, + // }; + // } + // if (isInValue && query[currentToken.stop + 1] === ' ') { + // return { + // tokenType: currentToken.type, + // text: currentToken.text, + // start: currentToken.start, + // stop: currentToken.stop, + // currentToken: currentToken.text, + // isInValue: false, + // isInKey: false, + // isInOperator: false, + // isInFunction: false, + // isInConjunction: true, + // isInParenthesis: false, + // }; + // } + // if (isInConjunction && query[currentToken.stop + 1] === ' ') { + // return { + // tokenType: currentToken.type, + // text: currentToken.text, + // start: currentToken.start, + // stop: currentToken.stop, + // currentToken: currentToken.text, + // isInValue: false, + // isInKey: true, + // isInOperator: false, + // isInFunction: false, + // isInConjunction: false, + // isInParenthesis: false, + // }; + // } + // if (isInParenthesis && query[currentToken.stop + 1] === ' ') { + // return { + // tokenType: currentToken.type, + // text: currentToken.text, + // start: currentToken.start, + // stop: currentToken.stop, + // currentToken: currentToken.text, + // isInValue: false, + // isInKey: false, // Suggest keys + // isInOperator: false, + // isInFunction: false, + // isInConjunction: true, // Suggest conjunctions + // isInParenthesis: false, + // }; + // } return { tokenType: currentToken.type, @@ -420,7 +420,7 @@ export function getQueryContextAtCursor( isInOperator, isInFunction, isInConjunction, - isInParenthesis, + // isInParenthesis, }; } catch (error) { console.error('Error in getQueryContextAtCursor:', error); @@ -439,3 +439,23 @@ export function getQueryContextAtCursor( }; } } + +export const queryOperatorSuggestions = [ + { label: '=', type: 'operator', info: 'Equal to' }, + { label: '!=', type: 'operator', info: 'Not equal to' }, + { label: '>', type: 'operator', info: 'Greater than' }, + { label: '<', type: 'operator', info: 'Less than' }, + { label: '>=', type: 'operator', info: 'Greater than or equal to' }, + { label: '<=', type: 'operator', info: 'Less than or equal to' }, + { label: 'LIKE', type: 'operator', info: 'Like' }, + { label: 'ILIKE', type: 'operator', info: 'Case insensitive like' }, + { label: 'BETWEEN', type: 'operator', info: 'Between' }, + { label: 'EXISTS', type: 'operator', info: 'Exists' }, + { label: 'REGEXP', type: 'operator', info: 'Regular expression' }, + { label: 'CONTAINS', type: 'operator', info: 'Contains' }, + { label: 'IN', type: 'operator', info: 'In' }, + { label: 'NOT', type: 'operator', info: 'Not' }, + { label: 'NOT_LIKE', type: 'operator', info: 'Not like' }, + { label: 'IS_NULL', type: 'operator', info: 'Is null' }, + { label: 'IS_NOT_NULL', type: 'operator', info: 'Is not null' }, +];