feat: handle multie select values better

This commit is contained in:
Yunus M 2025-05-06 01:01:45 +05:30 committed by ahrefabhi
parent a242fd3846
commit 104b14a478
3 changed files with 629 additions and 51 deletions

View File

@ -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 <Tag color="cyan">Function</Tag>;
case 'parenthesis':
return <Tag color="magenta">Parenthesis</Tag>;
case 'bracketList':
return <Tag color="red">Bracket List</Tag>;
default:
return <Tag>Unknown</Tag>;
}
@ -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() ?? '';

View File

@ -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;

View File

@ -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:
*