mirror of
https://github.com/SigNoz/signoz.git
synced 2025-12-25 19:40:24 +00:00
feat: handle multie select values better
This commit is contained in:
parent
67453e27f7
commit
4635da0ee8
@ -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() ?? '';
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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:
|
||||
*
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user