mirror of
https://github.com/SigNoz/signoz.git
synced 2026-06-04 08:00:26 +01:00
Compare commits
1 Commits
feat/v2-da
...
fix/query-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
906cb15010 |
@@ -648,3 +648,176 @@ describe('getQueryContextAtCursor - trailing dot in key/value', () => {
|
||||
expect(ctx.keyToken).toBe('k8s.namespace');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getQueryContextAtCursor - partial operator', () => {
|
||||
it('treats text after an incomplete key as an operator prefix', () => {
|
||||
const q = 'service.name c';
|
||||
const ctx = getQueryContextAtCursor(q, q.length);
|
||||
|
||||
expect(ctx.isInOperator).toBe(true);
|
||||
expect(ctx.isInKey).toBe(false);
|
||||
expect(ctx.keyToken).toBe('service.name');
|
||||
expect(ctx.operatorToken).toBe('c');
|
||||
expect(ctx.currentPair).toStrictEqual(
|
||||
expect.objectContaining({
|
||||
key: 'service.name',
|
||||
operator: 'c',
|
||||
position: expect.objectContaining({
|
||||
operatorStart: 13,
|
||||
operatorEnd: 13,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps the operator context while completing contains', () => {
|
||||
const q = 'service.name cont';
|
||||
const ctx = getQueryContextAtCursor(q, q.length);
|
||||
|
||||
expect(ctx.isInOperator).toBe(true);
|
||||
expect(ctx.keyToken).toBe('service.name');
|
||||
expect(ctx.operatorToken).toBe('cont');
|
||||
});
|
||||
|
||||
it('treats cursor mid-token as operator context', () => {
|
||||
const q = 'service.name cont';
|
||||
// cursor sits between "con" and "t" — user still typing the operator
|
||||
const ctx = getQueryContextAtCursor(q, 15);
|
||||
|
||||
expect(ctx.isInOperator).toBe(true);
|
||||
expect(ctx.isInKey).toBe(false);
|
||||
expect(ctx.keyToken).toBe('service.name');
|
||||
expect(ctx.operatorToken).toBe('cont');
|
||||
});
|
||||
|
||||
it('keeps operator context when an AND conjunction precedes the pair', () => {
|
||||
const q = 'a = 1 AND service.name c';
|
||||
const ctx = getQueryContextAtCursor(q, q.length);
|
||||
|
||||
expect(ctx.isInOperator).toBe(true);
|
||||
expect(ctx.isInKey).toBe(false);
|
||||
expect(ctx.keyToken).toBe('service.name');
|
||||
expect(ctx.operatorToken).toBe('c');
|
||||
expect(ctx.currentPair).toStrictEqual(
|
||||
expect.objectContaining({
|
||||
key: 'service.name',
|
||||
operator: 'c',
|
||||
position: expect.objectContaining({
|
||||
operatorStart: 23,
|
||||
operatorEnd: 23,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps operator context when an open parenthesis precedes the pair', () => {
|
||||
const q = '(service.name c';
|
||||
const ctx = getQueryContextAtCursor(q, q.length);
|
||||
|
||||
expect(ctx.isInOperator).toBe(true);
|
||||
expect(ctx.isInKey).toBe(false);
|
||||
expect(ctx.keyToken).toBe('service.name');
|
||||
expect(ctx.operatorToken).toBe('c');
|
||||
});
|
||||
|
||||
it('re-glues a partial operator that follows a NOT negation', () => {
|
||||
const q = 'service.name NOT c';
|
||||
const ctx = getQueryContextAtCursor(q, q.length);
|
||||
|
||||
expect(ctx.isInOperator).toBe(true);
|
||||
expect(ctx.isInKey).toBe(false);
|
||||
expect(ctx.keyToken).toBe('service.name');
|
||||
expect(ctx.operatorToken).toBe('NOT c');
|
||||
// operatorStart points at the partial operator (post-NOT), not at the
|
||||
// negation — so suggestion selection only replaces the partial, never
|
||||
// the user's typed NOT.
|
||||
expect(ctx.currentPair).toStrictEqual(
|
||||
expect.objectContaining({
|
||||
key: 'service.name',
|
||||
operator: 'NOT c',
|
||||
hasNegation: true,
|
||||
position: expect.objectContaining({
|
||||
negationStart: 13,
|
||||
negationEnd: 15,
|
||||
operatorStart: 17,
|
||||
operatorEnd: 17,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('re-glues a multi-character partial operator after NOT', () => {
|
||||
const q = 'service.name NOT lik';
|
||||
const ctx = getQueryContextAtCursor(q, q.length);
|
||||
|
||||
expect(ctx.isInOperator).toBe(true);
|
||||
expect(ctx.keyToken).toBe('service.name');
|
||||
expect(ctx.operatorToken).toBe('NOT lik');
|
||||
expect(ctx.currentPair?.hasNegation).toBe(true);
|
||||
});
|
||||
|
||||
it('re-glues an uppercase partial operator after NOT', () => {
|
||||
const q = 'service.name NOT EXI';
|
||||
const ctx = getQueryContextAtCursor(q, q.length);
|
||||
|
||||
expect(ctx.isInOperator).toBe(true);
|
||||
expect(ctx.keyToken).toBe('service.name');
|
||||
expect(ctx.operatorToken).toBe('NOT EXI');
|
||||
expect(ctx.currentPair?.hasNegation).toBe(true);
|
||||
});
|
||||
|
||||
it('preserves original NOT casing in the operator text', () => {
|
||||
const q = 'service.name not c';
|
||||
const ctx = getQueryContextAtCursor(q, q.length);
|
||||
|
||||
expect(ctx.isInOperator).toBe(true);
|
||||
expect(ctx.keyToken).toBe('service.name');
|
||||
expect(ctx.operatorToken).toBe('not c');
|
||||
expect(ctx.currentPair?.hasNegation).toBe(true);
|
||||
});
|
||||
|
||||
it('tolerates extra whitespace between NOT and the partial operator', () => {
|
||||
const q = 'service.name NOT c';
|
||||
const ctx = getQueryContextAtCursor(q, q.length);
|
||||
|
||||
expect(ctx.isInOperator).toBe(true);
|
||||
expect(ctx.keyToken).toBe('service.name');
|
||||
// Display text uses a canonical single space between NOT and the
|
||||
// partial, regardless of how many spaces the user typed.
|
||||
expect(ctx.operatorToken).toBe('NOT c');
|
||||
expect(ctx.currentPair?.hasNegation).toBe(true);
|
||||
});
|
||||
|
||||
it('keeps operator context for NOT-prefixed partial inside parentheses', () => {
|
||||
const q = '(service.name NOT c';
|
||||
const ctx = getQueryContextAtCursor(q, q.length);
|
||||
|
||||
expect(ctx.isInOperator).toBe(true);
|
||||
expect(ctx.keyToken).toBe('service.name');
|
||||
expect(ctx.operatorToken).toBe('NOT c');
|
||||
expect(ctx.currentPair?.hasNegation).toBe(true);
|
||||
});
|
||||
|
||||
it('keeps operator context for NOT-prefixed partial after an AND conjunction', () => {
|
||||
const q = 'a = 1 AND service.name NOT c';
|
||||
const ctx = getQueryContextAtCursor(q, q.length);
|
||||
|
||||
expect(ctx.isInOperator).toBe(true);
|
||||
expect(ctx.keyToken).toBe('service.name');
|
||||
expect(ctx.operatorToken).toBe('NOT c');
|
||||
expect(ctx.currentPair?.hasNegation).toBe(true);
|
||||
});
|
||||
|
||||
it('re-glues the most recent incomplete pair when three partial tokens are typed', () => {
|
||||
// Pins documented behavior: with two trailing partial pairs (`c` and
|
||||
// `k`), the heuristic pairs the most recent two — `c` becomes the
|
||||
// key, `k` becomes the partial operator. The earlier `service.name`
|
||||
// is dropped from the current pair view.
|
||||
const q = 'service.name c k';
|
||||
const ctx = getQueryContextAtCursor(q, q.length);
|
||||
|
||||
expect(ctx.isInOperator).toBe(true);
|
||||
expect(ctx.keyToken).toBe('c');
|
||||
expect(ctx.operatorToken).toBe('k');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -605,6 +605,98 @@ export function getQueryContextAtCursor(
|
||||
queryPairs,
|
||||
);
|
||||
|
||||
// Re-glue a partial operator that ANTLR has lexed as a second key.
|
||||
//
|
||||
// When the user types `service.name c` (or `service.name NOT c`), the
|
||||
// lexer sees two KEY tokens (`service.name`, `c`) instead of a key +
|
||||
// partial operator, so `extractQueryPairs` emits two consecutive
|
||||
// key-only incomplete pairs. Downstream, that makes the dropdown
|
||||
// suggest keys when it should be suggesting operators.
|
||||
//
|
||||
// Detect that pattern — a previous incomplete key-only pair followed
|
||||
// by another incomplete key-only `currentPair`, separated only by
|
||||
// whitespace (or by a negation token attached to the previous pair) —
|
||||
// and rebuild a single synthetic pair where the previous pair's key
|
||||
// is the key and the current pair's key is treated as the partial
|
||||
// operator. The synthetic pair inherits the previous pair's negation
|
||||
// flag and positions via the spread, so `NOT <partial>` propagates
|
||||
// correctly to consumers.
|
||||
const previousIncompletePair = queryPairs
|
||||
.filter(
|
||||
(pair) =>
|
||||
!pair.isComplete &&
|
||||
!!pair.key &&
|
||||
!pair.operator &&
|
||||
pair.position.keyEnd < (currentPair?.position.keyStart ?? cursorIndex),
|
||||
)
|
||||
.sort((a, b) => b.position.keyEnd - a.position.keyEnd)[0];
|
||||
|
||||
if (
|
||||
previousIncompletePair &&
|
||||
currentPair &&
|
||||
currentPair !== previousIncompletePair &&
|
||||
!currentPair.operator &&
|
||||
currentPair.position.keyStart > previousIncompletePair.position.keyEnd
|
||||
) {
|
||||
const negationStart = previousIncompletePair.position.negationStart ?? 0;
|
||||
const negationEnd = previousIncompletePair.position.negationEnd ?? 0;
|
||||
const negationAfterKey =
|
||||
previousIncompletePair.hasNegation &&
|
||||
negationStart > previousIncompletePair.position.keyEnd;
|
||||
const gapStart = negationAfterKey
|
||||
? negationEnd + 1
|
||||
: previousIncompletePair.position.keyEnd + 1;
|
||||
const textBetweenPairs = query.slice(
|
||||
gapStart,
|
||||
currentPair.position.keyStart,
|
||||
);
|
||||
|
||||
if (textBetweenPairs.trim() === '') {
|
||||
// The replacement range (operatorStart/operatorEnd) must point
|
||||
// at the partial operator only, NOT the leading negation.
|
||||
// Consumers like QuerySearch use it to splice the chosen
|
||||
// suggestion in-place, so including the negation would let a
|
||||
// `NOT lik` -> `LIKE` selection erase the user's typed `NOT`.
|
||||
// Matches the convention used for complete pairs in
|
||||
// extractQueryPairs, where operatorStart starts after the
|
||||
// negation token.
|
||||
const operatorStart = currentPair.position.keyStart;
|
||||
const operatorEnd = currentPair.position.keyEnd;
|
||||
const partialOperator = query.slice(operatorStart, operatorEnd + 1);
|
||||
const operatorText = negationAfterKey
|
||||
? `${query.slice(negationStart, negationEnd + 1)} ${partialOperator}`
|
||||
: partialOperator;
|
||||
|
||||
return {
|
||||
tokenType: -1,
|
||||
text: '',
|
||||
start: cursorIndex,
|
||||
stop: cursorIndex,
|
||||
currentToken: operatorText,
|
||||
isInKey: false,
|
||||
isInNegation: false,
|
||||
isInOperator: true,
|
||||
isInValue: false,
|
||||
isInConjunction: false,
|
||||
isInFunction: false,
|
||||
isInParenthesis: false,
|
||||
isInBracketList: false,
|
||||
keyToken: previousIncompletePair.key,
|
||||
operatorToken: operatorText,
|
||||
queryPairs,
|
||||
currentPair: {
|
||||
...previousIncompletePair,
|
||||
operator: operatorText,
|
||||
position: {
|
||||
...previousIncompletePair.position,
|
||||
operatorStart,
|
||||
operatorEnd,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 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 =
|
||||
|
||||
Reference in New Issue
Block a user