From 104b14a478606aeb7feaea3dec30f6abbdddb1db Mon Sep 17 00:00:00 2001 From: Yunus M Date: Tue, 6 May 2025 01:01:45 +0530 Subject: [PATCH] feat: handle multie select values better --- .../CodeMirrorWhereClause.tsx | 100 ++- frontend/src/types/antlrQueryTypes.ts | 1 + frontend/src/utils/queryContextUtils.ts | 579 ++++++++++++++++-- 3 files changed, 629 insertions(+), 51 deletions(-) diff --git a/frontend/src/components/QueryBuilderV2/CodeMirrorWhereClause/CodeMirrorWhereClause.tsx b/frontend/src/components/QueryBuilderV2/CodeMirrorWhereClause/CodeMirrorWhereClause.tsx index 085f1429a4b4..70d5b6466811 100644 --- a/frontend/src/components/QueryBuilderV2/CodeMirrorWhereClause/CodeMirrorWhereClause.tsx +++ b/frontend/src/components/QueryBuilderV2/CodeMirrorWhereClause/CodeMirrorWhereClause.tsx @@ -201,6 +201,7 @@ function CodeMirrorWhereClause(): JSX.Element { | 'conjunction' | 'function' | 'parenthesis' + | 'bracketList' | null >(null); @@ -245,11 +246,9 @@ function CodeMirrorWhereClause(): JSX.Element { return `[${value}]`; } - // For regular string values with regular operators - if ( - (type === 'value' || type === 'keyword') && - !isListOperator(operatorToken) - ) { + // If we're already inside bracket list for IN operator and it's a string value + // just wrap in quotes but not brackets (we're already in brackets) + if (type === 'value' || type === 'keyword') { return wrapStringValueInQuotes(value); } @@ -270,6 +269,16 @@ function CodeMirrorWhereClause(): JSX.Element { const isCursorBeforeToken = pos < doc.length && doc[pos] !== ' ' && doc[pos] !== undefined; + // Check brackets around cursor + const isCursorAtOpenBracket = + pos < doc.length && (doc[pos] === '[' || doc[pos] === '('); + const isCursorAfterOpenBracket = + pos > 0 && (doc[pos - 1] === '[' || doc[pos - 1] === '('); + const isCursorAtCloseBracket = + pos < doc.length && (doc[pos] === ']' || doc[pos] === ')'); + const isCursorAfterCloseBracket = + pos > 0 && (doc[pos - 1] === ']' || doc[pos - 1] === ')'); + // Check if cursor is at transition point (right after a token at the beginning of a space) const isTransitionPoint = isCursorAtSpace && isCursorAfterToken; @@ -295,6 +304,13 @@ function CodeMirrorWhereClause(): JSX.Element { cursorAfterToken: isCursorAfterToken, cursorBeforeToken: isCursorBeforeToken, isTransitionPoint, + bracketInfo: { + cursorAtOpenBracket: isCursorAtOpenBracket, + cursorAfterOpenBracket: isCursorAfterOpenBracket, + cursorAtCloseBracket: isCursorAtCloseBracket, + cursorAfterCloseBracket: isCursorAfterCloseBracket, + isInBracketList: context.isInBracketList, + }, contextType: context.isInKey ? 'Key' : context.isInOperator @@ -307,6 +323,8 @@ function CodeMirrorWhereClause(): JSX.Element { ? 'Function' : context.isInParenthesis ? 'Parenthesis' + : context.isInBracketList + ? 'BracketList' : 'Unknown', keyToken: context.keyToken, operatorToken: context.operatorToken, @@ -451,7 +469,7 @@ function CodeMirrorWhereClause(): JSX.Element { [activeKey, isLoadingSuggestions], ); - // Enhanced update handler to track context changes + // Enhanced update handler to track context changes, including bracket contexts const handleUpdate = useCallback( (viewUpdate: { view: EditorView }): void => { // Skip updates if component is unmounted @@ -485,6 +503,16 @@ function CodeMirrorWhereClause(): JSX.Element { pos > 0 && doc[pos - 1] !== ' ' && doc[pos - 1] !== undefined; const isTransitionPoint = isAtSpace && isAfterToken; + // Detect brackets around cursor + const isAtOpenBracket = + pos < doc.length && (doc[pos] === '[' || doc[pos] === '('); + const isAfterOpenBracket = + pos > 0 && (doc[pos - 1] === '[' || doc[pos - 1] === '('); + const isAtCloseBracket = + pos < doc.length && (doc[pos] === ']' || doc[pos] === ')'); + const isAfterCloseBracket = + pos > 0 && (doc[pos - 1] === ']' || doc[pos - 1] === ')'); + // Get context immediately when cursor position changes if (doc) { const context = getQueryContextAtCursor(doc, pos); @@ -503,6 +531,8 @@ function CodeMirrorWhereClause(): JSX.Element { ? 'function' : queryContext?.isInParenthesis ? 'parenthesis' + : queryContext?.isInBracketList + ? 'bracketList' : null; const newContextType = context.isInKey @@ -517,6 +547,8 @@ function CodeMirrorWhereClause(): JSX.Element { ? 'function' : context.isInParenthesis ? 'parenthesis' + : context.isInBracketList + ? 'bracketList' : null; // Log context changes for debugging @@ -530,6 +562,13 @@ function CodeMirrorWhereClause(): JSX.Element { isAtSpace, isAfterToken, isTransitionPoint, + bracketInfo: { + isAtOpenBracket, + isAfterOpenBracket, + isAtCloseBracket, + isAfterCloseBracket, + isInBracketList: context.isInBracketList, + }, keyToken: context.keyToken, operatorToken: context.operatorToken, valueToken: context.valueToken, @@ -591,6 +630,8 @@ function CodeMirrorWhereClause(): JSX.Element { return Function; case 'parenthesis': return Parenthesis; + case 'bracketList': + return Bracket List; default: return Unknown; } @@ -616,6 +657,53 @@ function CodeMirrorWhereClause(): JSX.Element { boost?: number; }[] = []; + // Special handling for bracket list context (for IN operator) + if (queryContext.isInBracketList) { + // If we're inside brackets for an IN operator, we want to show value suggestions + // but format them differently (just add quotes, don't wrap in brackets) + const keyName = queryContext.keyToken || queryContext.currentPair?.key || ''; + + if (!keyName) { + return null; + } + + // Trigger fetch only if needed + if (keyName && (keyName !== activeKey || isLoadingSuggestions)) { + // Don't trigger a new fetch if we're already loading for this key + if (!(isLoadingSuggestions && lastKeyRef.current === keyName)) { + fetchValueSuggestions(keyName); + } + } + + // For values in bracket list, just add quotes without enclosing in brackets + const processedOptions = valueSuggestions.map((option) => { + // Clone the option to avoid modifying the original + const processedOption = { ...option }; + + // Skip processing for non-selectable items + if (option.apply === false || typeof option.apply === 'function') { + return option; + } + + // For strings, just wrap in quotes (no brackets needed) + if (option.type === 'value' || option.type === 'keyword') { + processedOption.apply = wrapStringValueInQuotes(option.label); + processedOption.info = `Value for ${keyName} IN list`; + } else { + processedOption.apply = option.label; + processedOption.info = `Value for ${keyName} IN list`; + } + + return processedOption; + }); + + // Return current value suggestions without comma + return { + from: word?.from ?? 0, + options: processedOptions, + }; + } + if (queryContext.isInKey) { const searchText = word?.text.toLowerCase() ?? ''; diff --git a/frontend/src/types/antlrQueryTypes.ts b/frontend/src/types/antlrQueryTypes.ts index c2ca21ada06a..a7a3a54cb3d1 100644 --- a/frontend/src/types/antlrQueryTypes.ts +++ b/frontend/src/types/antlrQueryTypes.ts @@ -39,6 +39,7 @@ export interface IQueryContext { isInFunction: boolean; isInConjunction?: boolean; isInParenthesis?: boolean; + isInBracketList?: boolean; // For multi-value operators like IN where values are in brackets keyToken?: string; operatorToken?: string; valueToken?: string; diff --git a/frontend/src/utils/queryContextUtils.ts b/frontend/src/utils/queryContextUtils.ts index cb8c1a9d7384..95f491a7c5f8 100644 --- a/frontend/src/utils/queryContextUtils.ts +++ b/frontend/src/utils/queryContextUtils.ts @@ -140,6 +140,380 @@ function determineTokenContext( }; } +// Helper function to check if a token is an operator +function isOperatorToken(tokenType: number): boolean { + return [ + FilterQueryLexer.EQUALS, + FilterQueryLexer.NOT_EQUALS, + FilterQueryLexer.NEQ, + FilterQueryLexer.LT, + FilterQueryLexer.LE, + FilterQueryLexer.GT, + FilterQueryLexer.GE, + FilterQueryLexer.LIKE, + FilterQueryLexer.NOT_LIKE, + FilterQueryLexer.ILIKE, + FilterQueryLexer.NOT_ILIKE, + FilterQueryLexer.BETWEEN, + FilterQueryLexer.EXISTS, + FilterQueryLexer.REGEXP, + FilterQueryLexer.CONTAINS, + FilterQueryLexer.IN, + FilterQueryLexer.NOT, + ].includes(tokenType); +} + +// Helper function to check if a token is a value +function isValueToken(tokenType: number): boolean { + return [ + FilterQueryLexer.QUOTED_TEXT, + FilterQueryLexer.NUMBER, + FilterQueryLexer.BOOL, + ].includes(tokenType); +} + +// Helper function to check if a token is a conjunction +function isConjunctionToken(tokenType: number): boolean { + return [FilterQueryLexer.AND, FilterQueryLexer.OR].includes(tokenType); +} + +// Helper function to check if a token is a bracket +function isBracketToken(tokenType: number): boolean { + return [ + FilterQueryLexer.LPAREN, + FilterQueryLexer.RPAREN, + FilterQueryLexer.LBRACK, + FilterQueryLexer.RBRACK, + ].includes(tokenType); +} + +// Helper function to check if an operator typically uses bracket values (multi-value operators) +function isMultiValueOperator(operatorToken?: string): boolean { + if (!operatorToken) return false; + + const upperOp = operatorToken.toUpperCase(); + return upperOp === 'IN' || upperOp === 'NOT IN'; +} + +// Function to determine token context boundaries more precisely +function determineContextBoundaries( + query: string, + cursorIndex: number, + tokens: IToken[], + queryPairs: IQueryPair[], +): { + keyContext: { start: number; end: number } | null; + operatorContext: { start: number; end: number } | null; + valueContext: { start: number; end: number } | null; + conjunctionContext: { start: number; end: number } | null; + bracketContext: { start: number; end: number; isForList: boolean } | null; +} { + // Find the current query pair based on cursor position + let currentPair: IQueryPair | null = null; + + if (queryPairs.length > 0) { + // Look for the rightmost pair whose end position is before or at the cursor + let bestMatch: IQueryPair | null = null; + + for (const pair of queryPairs) { + const { position } = pair; + + // Find the rightmost position of this pair + const pairEnd = position.valueEnd || position.operatorEnd || position.keyEnd; + + // FIXED: Consider cursor position at the end of a token (including the last character) + if ( + (pairEnd <= cursorIndex || pairEnd + 1 === cursorIndex) && + (!bestMatch || + pairEnd > + (bestMatch.position.valueEnd || + bestMatch.position.operatorEnd || + bestMatch.position.keyEnd)) + ) { + bestMatch = pair; + } + } + + // If we found a match, use it + if (bestMatch) { + currentPair = bestMatch; + } + } + + // Check for bracket context first (could be part of an IN operator's value) + let bracketContext: { + start: number; + end: number; + isForList: boolean; + } | null = null; + + // Find bracket tokens that might contain the cursor + const openBrackets: { token: IToken; isForList: boolean }[] = []; + + for (let i = 0; i < tokens.length; i++) { + const token = tokens[i]; + + // Skip tokens on hidden channel + if (token.channel !== 0) continue; + + // Track opening brackets + if ( + token.type === FilterQueryLexer.LBRACK || + token.type === FilterQueryLexer.LPAREN + ) { + // Check if this opening bracket is for a list (used with IN operator) + let isForList = false; + + // Look back to see if this bracket follows an IN operator + if (i > 0) { + for (let j = i - 1; j >= 0; j--) { + const prevToken = tokens[j]; + if (prevToken.channel !== 0) continue; // Skip hidden channel tokens + + if ( + prevToken.type === FilterQueryLexer.IN || + (prevToken.type === FilterQueryLexer.NOT && + j + 1 < tokens.length && + tokens[j + 1].type === FilterQueryLexer.IN) + ) { + isForList = true; + break; + } else if (prevToken.channel === 0 && !isValueToken(prevToken.type)) { + // If we encounter a non-value token that's not IN, stop looking + break; + } + } + } + + openBrackets.push({ token, isForList }); + } + + // If this is a closing bracket, check if cursor is within this bracket pair + else if ( + (token.type === FilterQueryLexer.RBRACK || + token.type === FilterQueryLexer.RPAREN) && + openBrackets.length > 0 + ) { + const matchingOpen = openBrackets.pop(); + + // If cursor is within these brackets and they're for a list + if ( + matchingOpen && + matchingOpen.token.start <= cursorIndex && + cursorIndex <= token.stop + 1 + ) { + bracketContext = { + start: matchingOpen.token.start, + end: token.stop, + isForList: matchingOpen.isForList, + }; + break; + } + } + + // If we're at the cursor position and not in a closing bracket check + if (token.start <= cursorIndex && cursorIndex <= token.stop + 1) { + // If cursor is within an opening bracket token + if ( + token.type === FilterQueryLexer.LBRACK || + token.type === FilterQueryLexer.LPAREN + ) { + // Check if this is the start of a list for IN operator + let isForList = false; + + // Look back to see if this bracket follows an IN operator + for (let j = i - 1; j >= 0; j--) { + const prevToken = tokens[j]; + if (prevToken.channel !== 0) continue; // Skip hidden channel tokens + + if ( + prevToken.type === FilterQueryLexer.IN || + (prevToken.type === FilterQueryLexer.NOT && + j + 1 < tokens.length && + tokens[j + 1].type === FilterQueryLexer.IN) + ) { + isForList = true; + break; + } else if (prevToken.channel === 0) { + // If we encounter any token on the default channel, stop looking + break; + } + } + + bracketContext = { + start: token.start, + end: token.stop, + isForList, + }; + break; + } + + // If cursor is within a closing bracket token + if ( + token.type === FilterQueryLexer.RBRACK || + token.type === FilterQueryLexer.RPAREN + ) { + if (openBrackets.length > 0) { + const matchingOpen = openBrackets[openBrackets.length - 1]; + bracketContext = { + start: matchingOpen.token.start, + end: token.stop, + isForList: matchingOpen.isForList, + }; + } else { + bracketContext = { + start: token.start, + end: token.stop, + isForList: false, // We don't know, assume not list + }; + } + break; + } + } + } + + // If we have a current pair, determine context boundaries from it + if (currentPair) { + const { position } = currentPair; + + // Key context: from keyStart to keyEnd + const keyContext = { + start: position.keyStart, + end: position.keyEnd, + }; + + // Find the operator context start by looking for the first non-space character after keyEnd + let operatorStart = position.keyEnd + 1; + while (operatorStart < query.length && query[operatorStart] === ' ') { + operatorStart++; + } + + // Operator context: from first non-space after key to operatorEnd + const operatorContext = { + start: operatorStart, + end: position.operatorEnd, + }; + + // Find the value context start by looking for the first non-space character after operatorEnd + let valueStart = position.operatorEnd + 1; + while (valueStart < query.length && query[valueStart] === ' ') { + valueStart++; + } + + // Special handling for multi-value operators like IN + const isInOperator = isMultiValueOperator(currentPair.operator); + + // Value context: from first non-space after operator to valueEnd (if exists) + // If this is an IN operator and we're in a bracket context, use that instead + let valueContext = null; + + if (isInOperator && bracketContext && bracketContext.isForList) { + // For IN operator with brackets, the whole bracket content is the value context + valueContext = { + start: bracketContext.start, + end: bracketContext.end, + }; + } else if (position.valueEnd) { + valueContext = { + start: valueStart, + end: position.valueEnd, + }; + } + + // Look for conjunction after value (if value exists) + let conjunctionContext = null; + if (position.valueEnd) { + let conjunctionStart = position.valueEnd + 1; + while (conjunctionStart < query.length && query[conjunctionStart] === ' ') { + conjunctionStart++; + } + + // Check if there's a conjunction token after the value + for (const token of tokens) { + if ( + token.start === conjunctionStart && + (token.type === FilterQueryLexer.AND || token.type === FilterQueryLexer.OR) + ) { + conjunctionContext = { + start: conjunctionStart, + end: token.stop, + }; + break; + } + } + } + + return { + keyContext, + operatorContext, + valueContext, + conjunctionContext, + bracketContext, + }; + } + + // If no current pair but there might be a partial pair under construction, + // try to determine context from tokens directly + const tokenAtCursor = tokens.find( + (token) => + token.channel === 0 && + token.start <= cursorIndex && + cursorIndex <= token.stop + 1, + ); + + if (tokenAtCursor) { + // Check token type to determine context + if (tokenAtCursor.type === FilterQueryLexer.KEY) { + return { + keyContext: { start: tokenAtCursor.start, end: tokenAtCursor.stop }, + operatorContext: null, + valueContext: null, + conjunctionContext: null, + bracketContext, + }; + } + + if (isOperatorToken(tokenAtCursor.type)) { + return { + keyContext: null, + operatorContext: { start: tokenAtCursor.start, end: tokenAtCursor.stop }, + valueContext: null, + conjunctionContext: null, + bracketContext, + }; + } + + if (isValueToken(tokenAtCursor.type)) { + return { + keyContext: null, + operatorContext: null, + valueContext: { start: tokenAtCursor.start, end: tokenAtCursor.stop }, + conjunctionContext: null, + bracketContext, + }; + } + + if (isConjunctionToken(tokenAtCursor.type)) { + return { + keyContext: null, + operatorContext: null, + valueContext: null, + conjunctionContext: { start: tokenAtCursor.start, end: tokenAtCursor.stop }, + bracketContext, + }; + } + } + + // If no current pair, return null for all contexts except possibly bracket context + return { + keyContext: null, + operatorContext: null, + valueContext: null, + conjunctionContext: null, + bracketContext, + }; +} + /** * Gets the current query context at the cursor position * This is useful for determining what kind of suggestions to show @@ -179,6 +553,7 @@ export function getQueryContextAtCursor( isInFunction: false, isInConjunction: false, isInParenthesis: false, + isInBracketList: false, queryPairs: [], currentPair: null, }; @@ -189,9 +564,15 @@ export function getQueryContextAtCursor( const isAtSpace = cursorIndex < query.length && query[cursorIndex] === ' '; const isAfterSpace = cursorIndex > 0 && query[cursorIndex - 1] === ' '; const isAfterToken = cursorIndex > 0 && query[cursorIndex - 1] !== ' '; + const isBeforeToken = + cursorIndex < query.length && query[cursorIndex] !== ' '; // Check if cursor is right after a token and at the start of a space - const isTransitionPoint = isAtSpace && isAfterToken; + // FIXED: Consider the cursor to be at a transition point if it's at the end of a token + // and not yet at a space (this includes being at the end of the query) + const isTransitionPoint = + (isAtSpace && isAfterToken) || + (cursorIndex === query.length && isAfterToken); // First normalize the query to handle multiple spaces // We need to adjust cursorIndex based on space normalization @@ -241,13 +622,17 @@ export function getQueryContextAtCursor( const token = allTokens[i]; if (token.type === FilterQueryLexer.EOF) continue; - // Store this token if it's before or at the cursor position - if (token.stop < cursorIndex) { + // FIXED: Consider a token to be the lastTokenBeforeCursor if the cursor is + // exactly at the end of the token (including the last character) + if ( + token.stop < adjustedCursorIndex || + token.stop + 1 === adjustedCursorIndex + ) { lastTokenBeforeCursor = token; } // If we found a token that starts after the cursor, we're done searching - if (token.start > cursorIndex) { + if (token.start > adjustedCursorIndex) { break; } } @@ -268,9 +653,10 @@ export function getQueryContextAtCursor( const pairEnd = position.valueEnd || position.operatorEnd || position.keyEnd; - // If this pair ends at or before the cursor, and it's further right than our previous best match + // FIXED: If this pair ends at or before the cursor (including exactly at the end), + // and it's further right than our previous best match if ( - pairEnd <= cursorIndex && + (pairEnd <= adjustedCursorIndex || pairEnd + 1 === adjustedCursorIndex) && (!bestMatch || pairEnd > (bestMatch.position.valueEnd || @@ -286,11 +672,111 @@ export function getQueryContextAtCursor( currentPair = bestMatch; } // If cursor is at the end, use the last pair - else if (cursorIndex >= query.length) { + else if (adjustedCursorIndex >= input.length) { currentPair = queryPairs[queryPairs.length - 1]; } } + // Determine precise context boundaries + const contextBoundaries = determineContextBoundaries( + query, + adjustedCursorIndex, + allTokens, + queryPairs, + ); + + // Check if cursor is within any of the specific context boundaries + // FIXED: Include the case where the cursor is exactly at the end of a boundary + const isInKeyBoundary = + contextBoundaries.keyContext && + ((adjustedCursorIndex >= contextBoundaries.keyContext.start && + adjustedCursorIndex <= contextBoundaries.keyContext.end) || + adjustedCursorIndex === contextBoundaries.keyContext.end + 1); + + const isInOperatorBoundary = + contextBoundaries.operatorContext && + ((adjustedCursorIndex >= contextBoundaries.operatorContext.start && + adjustedCursorIndex <= contextBoundaries.operatorContext.end) || + adjustedCursorIndex === contextBoundaries.operatorContext.end + 1); + + const isInValueBoundary = + contextBoundaries.valueContext && + ((adjustedCursorIndex >= contextBoundaries.valueContext.start && + adjustedCursorIndex <= contextBoundaries.valueContext.end) || + adjustedCursorIndex === contextBoundaries.valueContext.end + 1); + + const isInConjunctionBoundary = + contextBoundaries.conjunctionContext && + ((adjustedCursorIndex >= contextBoundaries.conjunctionContext.start && + adjustedCursorIndex <= contextBoundaries.conjunctionContext.end) || + adjustedCursorIndex === contextBoundaries.conjunctionContext.end + 1); + + // Check for bracket list context (used for IN operator values) + const isInBracketListBoundary = + contextBoundaries.bracketContext && + contextBoundaries.bracketContext.isForList && + adjustedCursorIndex >= contextBoundaries.bracketContext.start && + adjustedCursorIndex <= contextBoundaries.bracketContext.end + 1; + + // Check for general parenthesis context (not for IN operator lists) + const isInParenthesisBoundary = + contextBoundaries.bracketContext && + !contextBoundaries.bracketContext.isForList && + adjustedCursorIndex >= contextBoundaries.bracketContext.start && + adjustedCursorIndex <= contextBoundaries.bracketContext.end + 1; + + // If cursor is within a specific context boundary, this takes precedence + if ( + isInKeyBoundary || + isInOperatorBoundary || + isInValueBoundary || + isInConjunctionBoundary || + isInBracketListBoundary || + isInParenthesisBoundary + ) { + // Extract information from the current pair (if available) + const keyToken = currentPair?.key || ''; + const operatorToken = currentPair?.operator || ''; + const valueToken = currentPair?.value || ''; + + // Determine if we're in a multi-value operator context + const isForMultiValueOperator = isMultiValueOperator(operatorToken); + + // If we're in a bracket list and it's for a multi-value operator like IN, + // treat it as part of the value context + const finalIsInValue = + isInValueBoundary || (isInBracketListBoundary && isForMultiValueOperator); + + return { + tokenType: -1, + text: '', + start: adjustedCursorIndex, + stop: adjustedCursorIndex, + currentToken: '', + isInKey: isInKeyBoundary || false, + isInOperator: isInOperatorBoundary || false, + isInValue: finalIsInValue || false, + isInConjunction: isInConjunctionBoundary || false, + isInFunction: false, + isInParenthesis: isInParenthesisBoundary || false, + isInBracketList: isInBracketListBoundary || false, + keyToken: isInKeyBoundary + ? keyToken + : isInOperatorBoundary || finalIsInValue + ? keyToken + : undefined, + operatorToken: isInOperatorBoundary + ? operatorToken + : finalIsInValue + ? operatorToken + : undefined, + valueToken: finalIsInValue ? valueToken : undefined, + queryPairs: queryPairs, + currentPair: currentPair, + }; + } + + // Continue with existing token-based logic for cases not covered by context boundaries // Handle cursor at the very end of input if (adjustedCursorIndex >= input.length && allTokens.length > 0) { const lastRealToken = allTokens @@ -310,7 +796,7 @@ export function getQueryContextAtCursor( continue; } - // Check if cursor is within token bounds (inclusive) + // FIXED: Check if cursor is within token bounds (inclusive) or exactly at the end if ( token.start <= adjustedCursorIndex && adjustedCursorIndex <= token.stop + 1 @@ -360,6 +846,7 @@ export function getQueryContextAtCursor( isInFunction: false, isInConjunction: false, isInParenthesis: false, + isInBracketList: false, queryPairs: queryPairs, // Add all query pairs to the context currentPair: null, // No current pair when query is empty }; @@ -388,6 +875,7 @@ export function getQueryContextAtCursor( isInFunction: false, isInConjunction: false, isInParenthesis: false, + isInBracketList: false, keyToken: lastTokenBeforeCursor.text, queryPairs: queryPairs, currentPair: currentPair, @@ -409,6 +897,7 @@ export function getQueryContextAtCursor( isInFunction: false, isInConjunction: false, isInParenthesis: false, + isInBracketList: false, operatorToken: lastTokenBeforeCursor.text, keyToken: keyFromPair, // Include key from current pair queryPairs: queryPairs, @@ -432,6 +921,7 @@ export function getQueryContextAtCursor( isInFunction: false, isInConjunction: true, // After value + space, should be conjunction context isInParenthesis: false, + isInBracketList: false, valueToken: lastTokenBeforeCursor.text, keyToken: keyFromPair, // Include key from current pair operatorToken: operatorFromPair, // Include operator from current pair @@ -454,12 +944,41 @@ export function getQueryContextAtCursor( isInFunction: false, isInConjunction: false, isInParenthesis: false, + isInBracketList: false, queryPairs: queryPairs, currentPair: currentPair, }; } } + // FIXED: Consider the case where the cursor is at the end of a token + // with no space yet (user is actively typing) + if (exactToken && adjustedCursorIndex === exactToken.stop + 1) { + const tokenContext = determineTokenContext(exactToken.type); + + // When the cursor is at the end of a token, return the current token context + return { + tokenType: exactToken.type, + text: exactToken.text, + start: exactToken.start, + stop: exactToken.stop, + currentToken: exactToken.text, + ...tokenContext, + isInBracketList: false, + keyToken: tokenContext.isInKey + ? exactToken.text + : currentPair?.key || undefined, + operatorToken: tokenContext.isInOperator + ? exactToken.text + : currentPair?.operator || undefined, + valueToken: tokenContext.isInValue + ? exactToken.text + : currentPair?.value || undefined, + queryPairs: queryPairs, + currentPair: currentPair, + }; + } + // Regular token-based context detection (when cursor is directly on a token) if (exactToken?.channel === 0) { const tokenContext = determineTokenContext(exactToken.type); @@ -476,6 +995,7 @@ export function getQueryContextAtCursor( stop: exactToken.stop, currentToken: exactToken.text, ...tokenContext, + isInBracketList: false, keyToken: tokenContext.isInKey ? exactToken.text : tokenContext.isInOperator || tokenContext.isInValue @@ -517,6 +1037,7 @@ export function getQueryContextAtCursor( isInFunction: false, isInConjunction: false, isInParenthesis: false, + isInBracketList: false, operatorToken: previousToken.text, keyToken: keyFromPair, // Include key from current pair queryPairs: queryPairs, @@ -538,6 +1059,7 @@ export function getQueryContextAtCursor( isInFunction: false, isInConjunction: false, isInParenthesis: false, + isInBracketList: false, keyToken: previousToken.text, queryPairs: queryPairs, currentPair: currentPair, @@ -557,6 +1079,7 @@ export function getQueryContextAtCursor( isInFunction: false, isInConjunction: true, // After value, progress to conjunction context isInParenthesis: false, + isInBracketList: false, valueToken: previousToken.text, keyToken: keyFromPair, // Include key from current pair operatorToken: operatorFromPair, // Include operator from current pair @@ -578,6 +1101,7 @@ export function getQueryContextAtCursor( isInFunction: false, isInConjunction: false, isInParenthesis: false, + isInBracketList: false, queryPairs: queryPairs, currentPair: currentPair, }; @@ -597,6 +1121,7 @@ export function getQueryContextAtCursor( isInFunction: false, isInConjunction: false, isInParenthesis: false, + isInBracketList: false, queryPairs: queryPairs, currentPair: currentPair, }; @@ -614,6 +1139,7 @@ export function getQueryContextAtCursor( isInFunction: false, isInConjunction: false, isInParenthesis: false, + isInBracketList: false, queryPairs: [], currentPair: null, }; @@ -857,43 +1383,6 @@ export function getCurrentQueryPair( } } -// Helper function to check if a token is an operator -function isOperatorToken(tokenType: number): boolean { - return [ - FilterQueryLexer.EQUALS, - FilterQueryLexer.NOT_EQUALS, - FilterQueryLexer.NEQ, - FilterQueryLexer.LT, - FilterQueryLexer.LE, - FilterQueryLexer.GT, - FilterQueryLexer.GE, - FilterQueryLexer.LIKE, - FilterQueryLexer.NOT_LIKE, - FilterQueryLexer.ILIKE, - FilterQueryLexer.NOT_ILIKE, - FilterQueryLexer.BETWEEN, - FilterQueryLexer.EXISTS, - FilterQueryLexer.REGEXP, - FilterQueryLexer.CONTAINS, - FilterQueryLexer.IN, - FilterQueryLexer.NOT, - ].includes(tokenType); -} - -// Helper function to check if a token is a value -function isValueToken(tokenType: number): boolean { - return [ - FilterQueryLexer.QUOTED_TEXT, - FilterQueryLexer.NUMBER, - FilterQueryLexer.BOOL, - ].includes(tokenType); -} - -// Helper function to check if a token is a conjunction -function isConjunctionToken(tokenType: number): boolean { - return [FilterQueryLexer.AND, FilterQueryLexer.OR].includes(tokenType); -} - /** * Usage example for query context with pairs: *