mirror of
https://github.com/SigNoz/signoz.git
synced 2025-12-28 15:48:12 +00:00
1554 lines
46 KiB
TypeScript
1554 lines
46 KiB
TypeScript
/* eslint-disable */
|
|
|
|
import { CharStreams, CommonTokenStream, Token } from 'antlr4';
|
|
import FilterQueryLexer from 'parser/FilterQueryLexer';
|
|
import { IQueryContext, IQueryPair, IToken } from 'types/antlrQueryTypes';
|
|
import { analyzeQuery } from 'parser/analyzeQuery';
|
|
import {
|
|
isBracketToken,
|
|
isConjunctionToken,
|
|
isFunctionToken,
|
|
isKeyToken,
|
|
isMultiValueOperator,
|
|
isNonValueOperatorToken,
|
|
isOperatorToken,
|
|
isQueryPairComplete,
|
|
isValueToken,
|
|
} from './tokenUtils';
|
|
import { NON_VALUE_OPERATORS } from 'constants/antlrQueryConstants';
|
|
|
|
// Function to normalize multiple spaces to single spaces when not in quotes
|
|
function normalizeSpaces(query: string): string {
|
|
let result = '';
|
|
let inQuotes = false;
|
|
let lastChar = '';
|
|
|
|
for (let i = 0; i < query.length; i++) {
|
|
const char = query[i];
|
|
|
|
// Track quote state
|
|
if (char === "'" && (i === 0 || query[i - 1] !== '\\')) {
|
|
inQuotes = !inQuotes;
|
|
}
|
|
|
|
// If we're in quotes, always keep the original character
|
|
if (inQuotes) {
|
|
result += char;
|
|
}
|
|
// Otherwise, collapse multiple spaces to a single space
|
|
else if (char === ' ' && lastChar === ' ') {
|
|
// Skip this space (don't add it)
|
|
} else {
|
|
result += char;
|
|
}
|
|
|
|
lastChar = char;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// Function to create a context object
|
|
export function createContext(
|
|
token: Token,
|
|
isInKey: boolean,
|
|
isInNegation: boolean,
|
|
isInOperator: boolean,
|
|
isInValue: boolean,
|
|
keyToken?: string,
|
|
operatorToken?: string,
|
|
valueToken?: string,
|
|
queryPairs?: IQueryPair[],
|
|
currentPair?: IQueryPair | null,
|
|
): IQueryContext {
|
|
return {
|
|
tokenType: token.type,
|
|
text: token.text || '',
|
|
start: token.start,
|
|
stop: token.stop,
|
|
currentToken: token.text || '',
|
|
isInKey,
|
|
isInNegation,
|
|
isInOperator,
|
|
isInValue,
|
|
isInFunction: false,
|
|
isInConjunction: false,
|
|
isInParenthesis: false,
|
|
keyToken,
|
|
operatorToken,
|
|
valueToken,
|
|
queryPairs: queryPairs || [],
|
|
currentPair,
|
|
};
|
|
}
|
|
|
|
// Helper to determine token type for context
|
|
function determineTokenContext(
|
|
token: IToken,
|
|
query: string,
|
|
): {
|
|
isInKey: boolean;
|
|
isInNegation: boolean;
|
|
isInOperator: boolean;
|
|
isInValue: boolean;
|
|
isInFunction: boolean;
|
|
isInConjunction: boolean;
|
|
isInParenthesis: boolean;
|
|
} {
|
|
let isInKey: boolean = false;
|
|
let isInNegation: boolean = false;
|
|
let isInOperator: boolean = false;
|
|
let isInValue: boolean = false;
|
|
let isInFunction: boolean = false;
|
|
let isInConjunction: boolean = false;
|
|
let isInParenthesis: boolean = false;
|
|
|
|
const tokenType = token.type;
|
|
const currentTokenContext = analyzeQuery(query, token);
|
|
|
|
if (!currentTokenContext) {
|
|
// Key context
|
|
isInKey = isKeyToken(tokenType);
|
|
|
|
// Operator context
|
|
isInOperator = isOperatorToken(tokenType);
|
|
|
|
// Value context
|
|
isInValue = isValueToken(tokenType);
|
|
} else {
|
|
switch (currentTokenContext.type) {
|
|
case 'Operator':
|
|
isInOperator = true;
|
|
break;
|
|
case 'Value':
|
|
isInValue = true;
|
|
break;
|
|
case 'Key':
|
|
isInKey = true;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Negation context
|
|
isInNegation = tokenType === FilterQueryLexer.NOT;
|
|
|
|
// Function context
|
|
isInFunction = isFunctionToken(tokenType);
|
|
|
|
// Conjunction context
|
|
isInConjunction = isConjunctionToken(tokenType);
|
|
|
|
// Parenthesis context
|
|
isInParenthesis = isBracketToken(tokenType);
|
|
|
|
return {
|
|
isInKey,
|
|
isInNegation,
|
|
isInOperator,
|
|
isInValue,
|
|
isInFunction,
|
|
isInConjunction,
|
|
isInParenthesis,
|
|
};
|
|
}
|
|
|
|
export function getCurrentValueIndexAtCursor(
|
|
valuesPosition: {
|
|
start?: number;
|
|
end?: number;
|
|
}[],
|
|
cursorIndex: number,
|
|
): number | null {
|
|
if (!valuesPosition || valuesPosition.length === 0) return null;
|
|
|
|
// Find the value that contains the cursor index
|
|
for (let i = 0; i < valuesPosition.length; i++) {
|
|
const start = valuesPosition[i].start;
|
|
const end = valuesPosition[i].end;
|
|
if (
|
|
start !== undefined &&
|
|
end !== undefined &&
|
|
start <= cursorIndex &&
|
|
cursorIndex <= end
|
|
) {
|
|
return i;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// 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;
|
|
negationContext: { 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) {
|
|
currentPair = getCurrentQueryPair(queryPairs, query, cursorIndex);
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
// Check if cursor is right after the closing bracket (with a space)
|
|
// We need to handle this case to transition to conjunction context
|
|
if (
|
|
matchingOpen &&
|
|
token.stop + 1 < cursorIndex &&
|
|
cursorIndex <= token.stop + 2 &&
|
|
query[token.stop + 1] === ' '
|
|
) {
|
|
// We'll set a special flag to indicate we're after a closing bracket
|
|
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;
|
|
|
|
// Negation context: from negationStart to negationEnd
|
|
const negationContext = {
|
|
start: position.negationStart ?? 0,
|
|
end: position.negationEnd ?? 0,
|
|
};
|
|
|
|
// 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,
|
|
negationContext,
|
|
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 },
|
|
negationContext: null,
|
|
operatorContext: null,
|
|
valueContext: null,
|
|
conjunctionContext: null,
|
|
bracketContext,
|
|
};
|
|
}
|
|
|
|
if (tokenAtCursor.type === FilterQueryLexer.NOT) {
|
|
return {
|
|
keyContext: null,
|
|
negationContext: { start: tokenAtCursor.start, end: tokenAtCursor.stop },
|
|
operatorContext: null,
|
|
valueContext: null,
|
|
conjunctionContext: null,
|
|
bracketContext,
|
|
};
|
|
}
|
|
|
|
if (isOperatorToken(tokenAtCursor.type)) {
|
|
return {
|
|
keyContext: null,
|
|
negationContext: null,
|
|
operatorContext: { start: tokenAtCursor.start, end: tokenAtCursor.stop },
|
|
valueContext: null,
|
|
conjunctionContext: null,
|
|
bracketContext,
|
|
};
|
|
}
|
|
|
|
if (isValueToken(tokenAtCursor.type)) {
|
|
return {
|
|
keyContext: null,
|
|
negationContext: null,
|
|
operatorContext: null,
|
|
valueContext: { start: tokenAtCursor.start, end: tokenAtCursor.stop },
|
|
conjunctionContext: null,
|
|
bracketContext,
|
|
};
|
|
}
|
|
|
|
if (isConjunctionToken(tokenAtCursor.type)) {
|
|
return {
|
|
keyContext: null,
|
|
negationContext: 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,
|
|
negationContext: 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
|
|
*
|
|
* The function now includes full query pair information:
|
|
* - queryPairs: All key-operator-value triplets in the query
|
|
* - currentPair: The pair at or before the current cursor position
|
|
*
|
|
* This enables more intelligent context-aware suggestions based on
|
|
* the current key, operator, and surrounding query structure.
|
|
*
|
|
* @param query The query string
|
|
* @param cursorIndex The position of the cursor in the query
|
|
* @returns The query context at the cursor position
|
|
*/
|
|
export function getQueryContextAtCursor(
|
|
query: string,
|
|
cursorIndex: number,
|
|
): IQueryContext {
|
|
try {
|
|
// Guard against infinite recursion by checking call stack
|
|
const stackTrace = new Error().stack || '';
|
|
const callCount = (stackTrace.match(/getQueryContextAtCursor/g) || []).length;
|
|
if (callCount > 3) {
|
|
console.warn(
|
|
'Potential infinite recursion detected in getQueryContextAtCursor',
|
|
);
|
|
return {
|
|
tokenType: -1,
|
|
text: '',
|
|
start: cursorIndex,
|
|
stop: cursorIndex,
|
|
currentToken: '',
|
|
isInKey: true,
|
|
isInNegation: false,
|
|
isInOperator: false,
|
|
isInValue: false,
|
|
isInFunction: false,
|
|
isInConjunction: false,
|
|
isInParenthesis: false,
|
|
isInBracketList: false,
|
|
queryPairs: [],
|
|
currentPair: null,
|
|
};
|
|
}
|
|
|
|
// First check if the cursor is at a token boundary or within a whitespace area
|
|
// This is critical for context detection
|
|
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
|
|
// 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
|
|
let adjustedCursorIndex = cursorIndex;
|
|
let spaceCount = 0;
|
|
let inQuotes = false;
|
|
|
|
// Count consecutive spaces before the cursor to adjust the cursor position
|
|
for (let i = 0; i < cursorIndex; i++) {
|
|
// Track quote state
|
|
if (query[i] === "'" && (i === 0 || query[i - 1] !== '\\')) {
|
|
inQuotes = !inQuotes;
|
|
}
|
|
|
|
// Only count spaces when not in quotes
|
|
if (!inQuotes && query[i] === ' ' && (i === 0 || query[i - 1] === ' ')) {
|
|
spaceCount++;
|
|
}
|
|
}
|
|
|
|
// Adjust cursor position based on removed spaces
|
|
adjustedCursorIndex = cursorIndex - spaceCount;
|
|
|
|
// Normalize the query by removing extra spaces when not in quotes
|
|
const normalizedQuery = normalizeSpaces(query);
|
|
|
|
// Create input stream and lexer with normalized query
|
|
const input = normalizedQuery || '';
|
|
const chars = CharStreams.fromString(input);
|
|
const lexer = new FilterQueryLexer(chars);
|
|
|
|
// Create token stream and force token generation
|
|
const tokenStream = new CommonTokenStream(lexer);
|
|
tokenStream.fill();
|
|
|
|
// Get all tokens including whitespace
|
|
const allTokens = tokenStream.tokens as IToken[];
|
|
|
|
// Find exact token at cursor, including whitespace
|
|
let exactToken: IToken | null = null;
|
|
let previousToken: IToken | null = null;
|
|
let nextToken: IToken | null = null;
|
|
|
|
// Find the real token at or just before the cursor
|
|
let lastTokenBeforeCursor: IToken | null = null;
|
|
for (let i = 0; i < allTokens.length; i++) {
|
|
const token = allTokens[i];
|
|
if (token.type === FilterQueryLexer.EOF) continue;
|
|
|
|
// 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 > adjustedCursorIndex) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Get query pairs information to enhance context
|
|
const queryPairs = extractQueryPairs(query);
|
|
|
|
// Find the current pair without causing a circular dependency
|
|
let currentPair: IQueryPair | null = null;
|
|
if (queryPairs.length > 0) {
|
|
currentPair = getCurrentQueryPair(queryPairs, query, adjustedCursorIndex);
|
|
}
|
|
|
|
// 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 isInNegationBoundary =
|
|
contextBoundaries.negationContext &&
|
|
((adjustedCursorIndex >= contextBoundaries.negationContext.start &&
|
|
adjustedCursorIndex <= contextBoundaries.negationContext.end) ||
|
|
adjustedCursorIndex === contextBoundaries.negationContext.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;
|
|
|
|
// Check if we're right after a closing bracket for a list (IN operator)
|
|
// This helps transition to conjunction context after a multi-value list
|
|
const isAfterClosingBracketList =
|
|
contextBoundaries.bracketContext &&
|
|
contextBoundaries.bracketContext.isForList &&
|
|
adjustedCursorIndex === contextBoundaries.bracketContext.end + 2 &&
|
|
query[contextBoundaries.bracketContext.end + 1] === ' ';
|
|
|
|
// If cursor is within a specific context boundary, this takes precedence
|
|
if (
|
|
isInKeyBoundary ||
|
|
isInNegationBoundary ||
|
|
isInOperatorBoundary ||
|
|
isInValueBoundary ||
|
|
isInConjunctionBoundary ||
|
|
isInBracketListBoundary ||
|
|
isInParenthesisBoundary ||
|
|
isAfterClosingBracketList
|
|
) {
|
|
// 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);
|
|
|
|
// If we're right after a closing bracket for a list, transition to conjunction context
|
|
const finalIsInConjunction =
|
|
isInConjunctionBoundary || isAfterClosingBracketList;
|
|
|
|
return {
|
|
tokenType: -1,
|
|
text: '',
|
|
start: adjustedCursorIndex,
|
|
stop: adjustedCursorIndex,
|
|
currentToken: '',
|
|
isInKey: isInKeyBoundary || false,
|
|
isInNegation: isInNegationBoundary || false,
|
|
isInOperator: isInOperatorBoundary || false,
|
|
isInValue: finalIsInValue || false,
|
|
isInConjunction: finalIsInConjunction || 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
|
|
.filter((t) => t.type !== FilterQueryLexer.EOF)
|
|
.pop();
|
|
if (lastRealToken) {
|
|
exactToken = lastRealToken;
|
|
previousToken =
|
|
allTokens.filter((t) => t.stop < lastRealToken.start).pop() || null;
|
|
}
|
|
} else {
|
|
// Normal token search
|
|
for (let i = 0; i < allTokens.length; i++) {
|
|
const token = allTokens[i];
|
|
// Skip EOF token in normal search
|
|
if (token.type === FilterQueryLexer.EOF) {
|
|
continue;
|
|
}
|
|
|
|
// FIXED: Check if cursor is within token bounds (inclusive) or exactly at the end
|
|
if (
|
|
token.start <= adjustedCursorIndex &&
|
|
adjustedCursorIndex <= token.stop + 1
|
|
) {
|
|
exactToken = token;
|
|
previousToken = i > 0 ? allTokens[i - 1] : null;
|
|
nextToken = i < allTokens.length - 1 ? allTokens[i + 1] : null;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If cursor is between tokens, find surrounding tokens
|
|
if (!exactToken) {
|
|
for (let i = 0; i < allTokens.length - 1; i++) {
|
|
const current = allTokens[i];
|
|
const next = allTokens[i + 1];
|
|
if (
|
|
current.type === FilterQueryLexer.EOF ||
|
|
next.type === FilterQueryLexer.EOF
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
if (
|
|
current.stop + 1 < adjustedCursorIndex &&
|
|
adjustedCursorIndex < next.start
|
|
) {
|
|
previousToken = current;
|
|
nextToken = next;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we don't have tokens yet, return default context
|
|
if (!previousToken && !nextToken && !exactToken && !lastTokenBeforeCursor) {
|
|
return {
|
|
tokenType: -1,
|
|
text: '',
|
|
start: adjustedCursorIndex,
|
|
stop: adjustedCursorIndex,
|
|
currentToken: '',
|
|
isInKey: true, // Default to key context when input is empty
|
|
isInNegation: false,
|
|
isInOperator: false,
|
|
isInValue: false,
|
|
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
|
|
};
|
|
}
|
|
|
|
// If we have a token and we're at a space after it (transition point),
|
|
// then we should progress the context
|
|
if (
|
|
lastTokenBeforeCursor &&
|
|
(isAtSpace || isAfterSpace || isTransitionPoint)
|
|
) {
|
|
const lastTokenContext = determineTokenContext(lastTokenBeforeCursor, input);
|
|
|
|
// Apply the context progression logic: key → operator → value → conjunction → key
|
|
if (lastTokenContext.isInKey) {
|
|
// If we just typed a key and then a space, we move to operator context
|
|
return {
|
|
tokenType: lastTokenBeforeCursor.type,
|
|
text: lastTokenBeforeCursor.text,
|
|
start: adjustedCursorIndex,
|
|
stop: adjustedCursorIndex,
|
|
currentToken: lastTokenBeforeCursor.text,
|
|
isInKey: false,
|
|
isInNegation: false,
|
|
isInOperator: true, // After key + space, should be operator context
|
|
isInValue: false,
|
|
isInFunction: false,
|
|
isInConjunction: false,
|
|
isInParenthesis: false,
|
|
isInBracketList: false,
|
|
keyToken: lastTokenBeforeCursor.text,
|
|
queryPairs: queryPairs,
|
|
currentPair: currentPair,
|
|
};
|
|
}
|
|
|
|
if (lastTokenContext.isInNegation) {
|
|
return {
|
|
tokenType: lastTokenBeforeCursor.type,
|
|
text: lastTokenBeforeCursor.text,
|
|
start: adjustedCursorIndex,
|
|
stop: adjustedCursorIndex,
|
|
currentToken: lastTokenBeforeCursor.text,
|
|
isInKey: false,
|
|
isInNegation: false,
|
|
isInOperator: true, // After key + space + NOT, should be operator context
|
|
isInValue: false,
|
|
isInFunction: false,
|
|
isInConjunction: false,
|
|
isInParenthesis: false,
|
|
isInBracketList: false,
|
|
keyToken: lastTokenBeforeCursor.text,
|
|
queryPairs: queryPairs,
|
|
currentPair: currentPair,
|
|
};
|
|
}
|
|
|
|
if (lastTokenContext.isInOperator) {
|
|
// If we just typed an operator and then a space, we move to value context
|
|
const keyFromPair = currentPair?.key || '';
|
|
const isNonValueToken = isNonValueOperatorToken(lastTokenBeforeCursor.type);
|
|
return {
|
|
tokenType: lastTokenBeforeCursor.type,
|
|
text: lastTokenBeforeCursor.text,
|
|
start: adjustedCursorIndex,
|
|
stop: adjustedCursorIndex,
|
|
currentToken: lastTokenBeforeCursor.text,
|
|
isInKey: false,
|
|
isInNegation: false,
|
|
isInOperator: false,
|
|
isInValue: !isNonValueToken, // After operator + space, should be value context
|
|
isInFunction: false,
|
|
isInConjunction: isNonValueToken,
|
|
isInParenthesis: false,
|
|
isInBracketList: false,
|
|
operatorToken: lastTokenBeforeCursor.text,
|
|
keyToken: keyFromPair, // Include key from current pair
|
|
queryPairs: queryPairs,
|
|
currentPair: currentPair,
|
|
};
|
|
}
|
|
|
|
if (lastTokenContext.isInValue) {
|
|
// If we just typed a value and then a space, we move to conjunction context
|
|
const keyFromPair = currentPair?.key || '';
|
|
const operatorFromPair = currentPair?.operator || '';
|
|
return {
|
|
tokenType: lastTokenBeforeCursor.type,
|
|
text: lastTokenBeforeCursor.text,
|
|
start: adjustedCursorIndex,
|
|
stop: adjustedCursorIndex,
|
|
currentToken: lastTokenBeforeCursor.text,
|
|
isInKey: false,
|
|
isInNegation: false,
|
|
isInOperator: false,
|
|
isInValue: false,
|
|
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
|
|
queryPairs: queryPairs,
|
|
currentPair: currentPair,
|
|
};
|
|
}
|
|
|
|
if (lastTokenContext.isInConjunction) {
|
|
// If we just typed a conjunction and then a space, we move to key context
|
|
return {
|
|
tokenType: lastTokenBeforeCursor.type,
|
|
text: lastTokenBeforeCursor.text,
|
|
start: adjustedCursorIndex,
|
|
stop: adjustedCursorIndex,
|
|
currentToken: lastTokenBeforeCursor.text,
|
|
isInKey: true, // After conjunction + space, should be key context
|
|
isInNegation: false,
|
|
isInOperator: false,
|
|
isInValue: false,
|
|
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, input);
|
|
|
|
// 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, input);
|
|
|
|
// Get relevant tokens based on current pair
|
|
const keyFromPair = currentPair?.key || '';
|
|
const operatorFromPair = currentPair?.operator || '';
|
|
const valueFromPair = currentPair?.value || '';
|
|
|
|
return {
|
|
tokenType: exactToken.type,
|
|
text: exactToken.text,
|
|
start: exactToken.start,
|
|
stop: exactToken.stop,
|
|
currentToken: exactToken.text,
|
|
...tokenContext,
|
|
isInBracketList: false,
|
|
keyToken: tokenContext.isInKey
|
|
? exactToken.text
|
|
: tokenContext.isInOperator || tokenContext.isInValue
|
|
? keyFromPair
|
|
: undefined,
|
|
operatorToken: tokenContext.isInOperator
|
|
? exactToken.text
|
|
: tokenContext.isInValue
|
|
? operatorFromPair
|
|
: undefined,
|
|
valueToken: tokenContext.isInValue ? exactToken.text : undefined,
|
|
queryPairs: queryPairs,
|
|
currentPair: currentPair,
|
|
};
|
|
}
|
|
|
|
// If we're between tokens but not after a space, use previous token to determine context
|
|
if (previousToken?.channel === 0) {
|
|
const prevContext = determineTokenContext(previousToken, input);
|
|
|
|
// Get relevant tokens based on current pair
|
|
const keyFromPair = currentPair?.key || '';
|
|
const operatorFromPair = currentPair?.operator || '';
|
|
const valueFromPair = currentPair?.value || '';
|
|
|
|
// CRITICAL FIX: Check if the last meaningful token is an operator
|
|
// If so, we're always in the value context regardless of spaces
|
|
if (prevContext.isInOperator) {
|
|
// If previous token is operator, we must be in value context
|
|
return {
|
|
tokenType: previousToken.type,
|
|
text: previousToken.text,
|
|
start: adjustedCursorIndex,
|
|
stop: adjustedCursorIndex,
|
|
currentToken: previousToken.text,
|
|
isInKey: false,
|
|
isInNegation: false,
|
|
isInOperator: false,
|
|
isInValue: true, // Always in value context after operator
|
|
isInFunction: false,
|
|
isInConjunction: false,
|
|
isInParenthesis: false,
|
|
isInBracketList: false,
|
|
operatorToken: previousToken.text,
|
|
keyToken: keyFromPair, // Include key from current pair
|
|
queryPairs: queryPairs,
|
|
currentPair: currentPair,
|
|
};
|
|
}
|
|
|
|
// Maintain the strict progression key → operator → value → conjunction → key
|
|
if (prevContext.isInKey) {
|
|
return {
|
|
tokenType: previousToken.type,
|
|
text: previousToken.text,
|
|
start: adjustedCursorIndex,
|
|
stop: adjustedCursorIndex,
|
|
currentToken: previousToken.text,
|
|
isInKey: false,
|
|
isInNegation: false,
|
|
isInOperator: true, // After key, progress to operator context
|
|
isInValue: false,
|
|
isInFunction: false,
|
|
isInConjunction: false,
|
|
isInParenthesis: false,
|
|
isInBracketList: false,
|
|
keyToken: previousToken.text,
|
|
queryPairs: queryPairs,
|
|
currentPair: currentPair,
|
|
};
|
|
}
|
|
|
|
if (prevContext.isInValue) {
|
|
return {
|
|
tokenType: previousToken.type,
|
|
text: previousToken.text,
|
|
start: adjustedCursorIndex,
|
|
stop: adjustedCursorIndex,
|
|
currentToken: previousToken.text,
|
|
isInKey: false,
|
|
isInNegation: false,
|
|
isInOperator: false,
|
|
isInValue: false,
|
|
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
|
|
queryPairs: queryPairs,
|
|
currentPair: currentPair,
|
|
};
|
|
}
|
|
|
|
if (prevContext.isInConjunction) {
|
|
return {
|
|
tokenType: previousToken.type,
|
|
text: previousToken.text,
|
|
start: adjustedCursorIndex,
|
|
stop: adjustedCursorIndex,
|
|
currentToken: previousToken.text,
|
|
isInKey: true, // After conjunction, progress back to key context
|
|
isInNegation: false,
|
|
isInOperator: false,
|
|
isInValue: false,
|
|
isInFunction: false,
|
|
isInConjunction: false,
|
|
isInParenthesis: false,
|
|
isInBracketList: false,
|
|
queryPairs: queryPairs,
|
|
currentPair: currentPair,
|
|
};
|
|
}
|
|
}
|
|
|
|
// Default fallback to key context
|
|
return {
|
|
tokenType: -1,
|
|
text: '',
|
|
start: adjustedCursorIndex,
|
|
stop: adjustedCursorIndex,
|
|
currentToken: '',
|
|
isInKey: true,
|
|
isInNegation: false,
|
|
isInOperator: false,
|
|
isInValue: false,
|
|
isInFunction: false,
|
|
isInConjunction: false,
|
|
isInParenthesis: false,
|
|
isInBracketList: false,
|
|
queryPairs: queryPairs,
|
|
currentPair: currentPair,
|
|
};
|
|
} catch (error) {
|
|
console.error('Error in getQueryContextAtCursor:', error);
|
|
return {
|
|
tokenType: -1,
|
|
text: '',
|
|
start: cursorIndex,
|
|
stop: cursorIndex,
|
|
currentToken: '',
|
|
isInValue: false,
|
|
isInKey: true, // Default to key context on error
|
|
isInNegation: false,
|
|
isInOperator: false,
|
|
isInFunction: false,
|
|
isInConjunction: false,
|
|
isInParenthesis: false,
|
|
isInBracketList: false,
|
|
queryPairs: [],
|
|
currentPair: null,
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extracts all key-operator-value triplets from a query string
|
|
* This is useful for getting value suggestions based on the current key and operator
|
|
*
|
|
* @param query The query string to parse
|
|
* @returns An array of IQueryPair objects representing the key-operator-value triplets
|
|
*/
|
|
export function extractQueryPairs(query: string): IQueryPair[] {
|
|
try {
|
|
// Guard against infinite recursion by checking call stack
|
|
const stackTrace = new Error().stack || '';
|
|
const callCount = (stackTrace.match(/extractQueryPairs/g) || []).length;
|
|
if (callCount > 3) {
|
|
console.warn('Potential infinite recursion detected in extractQueryPairs');
|
|
return [];
|
|
}
|
|
|
|
// Normalize the query to handle multiple spaces
|
|
const normalizedQuery = normalizeSpaces(query);
|
|
|
|
// Create input stream and lexer with normalized query
|
|
const input = normalizedQuery || '';
|
|
const chars = CharStreams.fromString(input);
|
|
const lexer = new FilterQueryLexer(chars);
|
|
|
|
// Create token stream and force token generation
|
|
const tokenStream = new CommonTokenStream(lexer);
|
|
tokenStream.fill();
|
|
|
|
// Get all tokens including whitespace
|
|
const allTokens = tokenStream.tokens as IToken[];
|
|
|
|
const queryPairs: IQueryPair[] = [];
|
|
let currentPair: Partial<IQueryPair> | null = null;
|
|
|
|
let iterator = 0;
|
|
|
|
// Process tokens to build triplets
|
|
while (iterator < allTokens.length) {
|
|
const token = allTokens[iterator];
|
|
iterator += 1;
|
|
|
|
// Skip EOF and whitespace tokens
|
|
if (token.type === FilterQueryLexer.EOF || token.channel !== 0) {
|
|
continue;
|
|
}
|
|
|
|
// If token is a KEY, start a new pair
|
|
if (
|
|
token.type === FilterQueryLexer.KEY &&
|
|
!(currentPair && currentPair.key)
|
|
) {
|
|
// If we have an existing incomplete pair, add it to the result
|
|
if (currentPair && currentPair.key) {
|
|
queryPairs.push({
|
|
key: currentPair.key,
|
|
operator: currentPair.operator || '',
|
|
value: currentPair.value,
|
|
valueList: currentPair.valueList || [],
|
|
valuesPosition: currentPair.valuesPosition || [],
|
|
hasNegation: currentPair.hasNegation || false,
|
|
isMultiValue: currentPair.isMultiValue || false,
|
|
position: {
|
|
keyStart: currentPair.position?.keyStart || 0,
|
|
keyEnd: currentPair.position?.keyEnd || 0,
|
|
operatorStart: currentPair.position?.operatorStart || 0,
|
|
operatorEnd: currentPair.position?.operatorEnd || 0,
|
|
valueStart: currentPair.position?.valueStart,
|
|
valueEnd: currentPair.position?.valueEnd,
|
|
negationStart: currentPair.position?.negationStart || 0,
|
|
negationEnd: currentPair.position?.negationEnd || 0,
|
|
},
|
|
isComplete: !!(
|
|
currentPair.key &&
|
|
currentPair.operator &&
|
|
currentPair.value
|
|
),
|
|
} as IQueryPair);
|
|
}
|
|
|
|
// Start a new pair
|
|
currentPair = {
|
|
key: token.text,
|
|
position: {
|
|
keyStart: token.start,
|
|
keyEnd: token.stop,
|
|
operatorStart: 0, // Initialize with default values
|
|
operatorEnd: 0, // Initialize with default values
|
|
},
|
|
};
|
|
}
|
|
// If NOT token comes set hasNegation to true
|
|
else if (token.type === FilterQueryLexer.NOT && currentPair) {
|
|
currentPair.hasNegation = true;
|
|
|
|
currentPair.position = {
|
|
keyStart: currentPair.position?.keyStart || 0,
|
|
keyEnd: currentPair.position?.keyEnd || 0,
|
|
operatorStart: currentPair.position?.operatorStart || 0,
|
|
operatorEnd: currentPair.position?.operatorEnd || 0,
|
|
valueStart: currentPair.position?.valueStart,
|
|
valueEnd: currentPair.position?.valueEnd,
|
|
negationStart: token.start,
|
|
negationEnd: token.stop,
|
|
};
|
|
}
|
|
|
|
// If token is an operator and we have a key, add the operator
|
|
else if (
|
|
isOperatorToken(token.type) &&
|
|
currentPair &&
|
|
currentPair.key &&
|
|
!currentPair.operator
|
|
) {
|
|
let multiValueStart: number | undefined;
|
|
let multiValueEnd: number | undefined;
|
|
|
|
if (isMultiValueOperator(token.text)) {
|
|
currentPair.isMultiValue = true;
|
|
|
|
// Iterate from '[' || '(' till ']' || ')' to get all the values
|
|
const valueList: string[] = [];
|
|
const valuesPosition: { start: number; end: number }[] = [];
|
|
|
|
if (
|
|
[FilterQueryLexer.LPAREN, FilterQueryLexer.LBRACK].includes(
|
|
allTokens[iterator].type,
|
|
)
|
|
) {
|
|
multiValueStart = allTokens[iterator].start;
|
|
iterator += 1;
|
|
const closingToken =
|
|
allTokens[iterator].type === FilterQueryLexer.LPAREN
|
|
? FilterQueryLexer.RPAREN
|
|
: FilterQueryLexer.RBRACK;
|
|
|
|
while (
|
|
allTokens[iterator].type !== closingToken &&
|
|
iterator < allTokens.length
|
|
) {
|
|
if (isValueToken(allTokens[iterator].type)) {
|
|
valueList.push(allTokens[iterator].text);
|
|
valuesPosition.push({
|
|
start: allTokens[iterator].start,
|
|
end: allTokens[iterator].stop,
|
|
});
|
|
}
|
|
iterator += 1;
|
|
}
|
|
|
|
if (allTokens[iterator].type === closingToken) {
|
|
multiValueEnd = allTokens[iterator].stop;
|
|
}
|
|
}
|
|
|
|
currentPair.valuesPosition = valuesPosition;
|
|
currentPair.valueList = valueList;
|
|
|
|
if (multiValueStart && multiValueEnd) {
|
|
currentPair.value = query.substring(multiValueStart, multiValueEnd + 1);
|
|
}
|
|
}
|
|
|
|
currentPair.operator = token.text;
|
|
// Ensure we create a valid position object with all required fields
|
|
currentPair.position = {
|
|
keyStart: currentPair.position?.keyStart || 0,
|
|
keyEnd: currentPair.position?.keyEnd || 0,
|
|
operatorStart: token.start,
|
|
operatorEnd: token.stop,
|
|
valueStart: multiValueStart || currentPair.position?.valueStart,
|
|
valueEnd: multiValueEnd || currentPair.position?.valueEnd,
|
|
negationStart: currentPair.position?.negationStart || 0,
|
|
negationEnd: currentPair.position?.negationEnd || 0,
|
|
};
|
|
}
|
|
// If token is a value and we have a key and operator, add the value
|
|
else if (
|
|
isValueToken(token.type) &&
|
|
currentPair &&
|
|
currentPair.key &&
|
|
currentPair.operator &&
|
|
!NON_VALUE_OPERATORS.includes(currentPair.operator) &&
|
|
!currentPair.value
|
|
) {
|
|
currentPair.value = token.text;
|
|
// Ensure we create a valid position object with all required fields
|
|
currentPair.position = {
|
|
keyStart: currentPair.position?.keyStart || 0,
|
|
keyEnd: currentPair.position?.keyEnd || 0,
|
|
operatorStart: currentPair.position?.operatorStart || 0,
|
|
operatorEnd: currentPair.position?.operatorEnd || 0,
|
|
valueStart: token.start,
|
|
valueEnd: token.stop,
|
|
negationStart: currentPair.position?.negationStart || 0,
|
|
negationEnd: currentPair.position?.negationEnd || 0,
|
|
};
|
|
}
|
|
// If token is a conjunction (AND/OR) or A key, finalize the current pair
|
|
else if (
|
|
currentPair &&
|
|
currentPair.key &&
|
|
(isConjunctionToken(token.type) ||
|
|
(token.type === FilterQueryLexer.KEY && isQueryPairComplete(currentPair)))
|
|
) {
|
|
queryPairs.push({
|
|
key: currentPair.key,
|
|
operator: currentPair.operator || '',
|
|
value: currentPair.value,
|
|
valueList: currentPair.valueList || [],
|
|
valuesPosition: currentPair.valuesPosition || [],
|
|
hasNegation: currentPair.hasNegation || false,
|
|
isMultiValue: currentPair.isMultiValue || false,
|
|
position: {
|
|
keyStart: currentPair.position?.keyStart || 0,
|
|
keyEnd: currentPair.position?.keyEnd || 0,
|
|
operatorStart: currentPair.position?.operatorStart || 0,
|
|
operatorEnd: currentPair.position?.operatorEnd || 0,
|
|
valueStart: currentPair.position?.valueStart,
|
|
valueEnd: currentPair.position?.valueEnd,
|
|
negationStart: currentPair.position?.negationStart || 0,
|
|
negationEnd: currentPair.position?.negationEnd || 0,
|
|
},
|
|
isComplete: !!(
|
|
currentPair.key &&
|
|
currentPair.operator &&
|
|
currentPair.value
|
|
),
|
|
} as IQueryPair);
|
|
|
|
// Reset for the next pair
|
|
currentPair = null;
|
|
|
|
if (token.type === FilterQueryLexer.KEY) {
|
|
// If we encounter a new key, start a new pair immediately
|
|
currentPair = {
|
|
key: token.text,
|
|
position: {
|
|
keyStart: token.start,
|
|
keyEnd: token.stop,
|
|
operatorStart: 0, // Initialize with default values
|
|
operatorEnd: 0, // Initialize with default values
|
|
},
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add the last pair if not already added
|
|
if (currentPair && currentPair.key) {
|
|
queryPairs.push({
|
|
key: currentPair.key,
|
|
operator: currentPair.operator || '',
|
|
value: currentPair.value,
|
|
valueList: currentPair.valueList || [],
|
|
valuesPosition: currentPair.valuesPosition || [],
|
|
hasNegation: currentPair.hasNegation || false,
|
|
isMultiValue: currentPair.isMultiValue || false,
|
|
position: {
|
|
keyStart: currentPair.position?.keyStart || 0,
|
|
keyEnd: currentPair.position?.keyEnd || 0,
|
|
operatorStart: currentPair.position?.operatorStart || 0,
|
|
operatorEnd: currentPair.position?.operatorEnd || 0,
|
|
valueStart: currentPair.position?.valueStart,
|
|
valueEnd: currentPair.position?.valueEnd,
|
|
negationStart: currentPair.position?.negationStart || 0,
|
|
negationEnd: currentPair.position?.negationEnd || 0,
|
|
},
|
|
isComplete: !!(
|
|
currentPair.key &&
|
|
currentPair.operator &&
|
|
currentPair.value
|
|
),
|
|
} as IQueryPair);
|
|
}
|
|
|
|
return queryPairs;
|
|
} catch (error) {
|
|
console.error('Error in extractQueryPairs:', error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the current query pair at the cursor position
|
|
* This is useful for getting suggestions based on the current context
|
|
* The function finds the rightmost complete pair that ends before or at the cursor position
|
|
*
|
|
* @param queryPairs An array of IQueryPair objects representing the key-operator-value triplets
|
|
* @param query The full query string
|
|
* @param cursorIndex The position of the cursor in the query
|
|
* @returns The query pair at the cursor position, or null if not found
|
|
*/
|
|
export function getCurrentQueryPair(
|
|
queryPairs: IQueryPair[],
|
|
query: string,
|
|
cursorIndex: number,
|
|
): IQueryPair | null {
|
|
try {
|
|
// If we have pairs, try to find the one at the cursor position
|
|
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;
|
|
|
|
const pairStart =
|
|
position.keyStart || position.operatorStart || position.valueStart || 0;
|
|
|
|
// If this pair ends at or before the cursor, and it's further right than our previous best match
|
|
if (
|
|
pairEnd >= cursorIndex &&
|
|
pairStart <= cursorIndex &&
|
|
(!bestMatch ||
|
|
pairEnd >
|
|
(bestMatch.position.valueEnd ||
|
|
bestMatch.position.operatorEnd ||
|
|
bestMatch.position.keyEnd))
|
|
) {
|
|
bestMatch = pair;
|
|
}
|
|
}
|
|
|
|
// If we found a match, return it
|
|
if (bestMatch) {
|
|
return bestMatch;
|
|
}
|
|
|
|
// If cursor is at the very beginning, before any pairs, return null
|
|
if (cursorIndex === 0) {
|
|
return null;
|
|
}
|
|
|
|
// If no match found and cursor is at the end, return the last pair
|
|
if (cursorIndex >= query.length && queryPairs.length > 0) {
|
|
return queryPairs[queryPairs.length - 1];
|
|
}
|
|
}
|
|
|
|
// If no valid pair is found, and we cannot infer one from context, return null
|
|
return null;
|
|
} catch (error) {
|
|
console.error('Error in getCurrentQueryPair:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Usage example for query context with pairs:
|
|
*
|
|
* ```typescript
|
|
* // Get context at cursor position
|
|
* const context = getQueryContextAtCursor(query, cursorPosition);
|
|
*
|
|
* // Access all query pairs
|
|
* const allPairs = context.queryPairs || [];
|
|
* console.log(`Query contains ${allPairs.length} key-operator-value triplets`);
|
|
*
|
|
* // Access the current pair at cursor
|
|
* if (context.currentPair) {
|
|
* // Use the current triplet to provide relevant suggestions
|
|
* const { key, operator, value } = context.currentPair;
|
|
* console.log(`Current context: ${key} ${operator} ${value || ''}`);
|
|
*
|
|
* // Check if this is a complete triplet
|
|
* if (context.currentPair.isComplete) {
|
|
* // All parts (key, operator, value) are present
|
|
* } else {
|
|
* // Incomplete - might be missing operator or value
|
|
* }
|
|
* } else {
|
|
* // No current pair, likely at the start of a new condition
|
|
* }
|
|
* ```
|
|
*/
|