From 5fae64ccbe22ae37bc95cde1b173a47628a494ff Mon Sep 17 00:00:00 2001 From: Fabrizio <135109076+fabrizio-grafana@users.noreply.github.com> Date: Thu, 28 Sep 2023 15:48:18 +0200 Subject: [PATCH] Properly manage trigger chars with open editor box (#75461) --- .../tempo/traceql/autocomplete.test.ts | 27 +++++++-- .../tempo/traceql/situation.test.ts | 7 ++- .../datasource/tempo/traceql/situation.ts | 56 ++++++++++++++----- 3 files changed, 69 insertions(+), 21 deletions(-) diff --git a/public/app/plugins/datasource/tempo/traceql/autocomplete.test.ts b/public/app/plugins/datasource/tempo/traceql/autocomplete.test.ts index 665a96b5d5a..bba93e29820 100644 --- a/public/app/plugins/datasource/tempo/traceql/autocomplete.test.ts +++ b/public/app/plugins/datasource/tempo/traceql/autocomplete.test.ts @@ -251,19 +251,14 @@ describe('CompletionProvider', () => { }); it.each([ - ['{ .foo }', 6], ['{ .foo }', 7], - ['{.foo 300}', 5], ['{.foo 300}', 6], ['{.foo 300}', 7], ['{.foo 300}', 8], - ['{.foo 300 && .bar = 200}', 5], ['{.foo 300 && .bar = 200}', 6], ['{.foo 300 && .bar = 200}', 7], - ['{.foo 300 && .bar 200}', 18], ['{.foo 300 && .bar 200}', 19], ['{.foo 300 && .bar 200}', 20], - ['{ .foo = 1 && .bar }', 18], ['{ .foo = 1 && .bar }', 19], ['{ .foo = 1 && .bar }', 19], ])('suggests with incomplete spanset - %s, %i', async (input: string, offset: number) => { @@ -276,6 +271,28 @@ describe('CompletionProvider', () => { ); }); + it.each([ + ['{ .foo }', 6], + ['{.foo 300}', 5], + ['{.foo 300 && .bar = 200}', 5], + ['{ .foo = 1 && .bar }', 18], + ])('suggests with incomplete spanset with no space before cursor - %s, %i', async (input: string, offset: number) => { + const { provider, model } = setup(input, offset); + const result = await provider.provideCompletionItems(model, emptyPosition); + expect((result! as monacoTypes.languages.CompletionList).suggestions).toEqual([]); + }); + + it.each([ + ['{ span.d }', 8], + ['{ span.db }', 9], + ])('suggests to complete attribute - %s, %i', async (input: string, offset: number) => { + const { provider, model } = setup(input, offset, undefined, v2Tags); + const result = await provider.provideCompletionItems(model, emptyPosition); + expect((result! as monacoTypes.languages.CompletionList).suggestions).toEqual([ + expect.objectContaining({ label: 'db', insertText: 'db' }), + ]); + }); + it.each([ ['{.foo=1} {.bar=2}', 8], ['{.foo=1} {.bar=2}', 9], diff --git a/public/app/plugins/datasource/tempo/traceql/situation.test.ts b/public/app/plugins/datasource/tempo/traceql/situation.test.ts index bda871ba18e..8f6dc0da1da 100644 --- a/public/app/plugins/datasource/tempo/traceql/situation.test.ts +++ b/public/app/plugins/datasource/tempo/traceql/situation.test.ts @@ -25,6 +25,11 @@ describe('situation', () => { { query: '{.foo}', cursorPos: 5, + expected: { type: 'SPANSET_IN_NAME_SCOPE', scope: '' }, + }, + { + query: '{.foo }', + cursorPos: 6, expected: { type: 'SPANSET_EXPRESSION_OPERATORS' }, }, { @@ -49,7 +54,7 @@ describe('situation', () => { }, { query: '{span.foo = 200 }', - cursorPos: 15, + cursorPos: 16, expected: { type: 'SPANFIELD_COMBINING_OPERATORS' }, }, { diff --git a/public/app/plugins/datasource/tempo/traceql/situation.ts b/public/app/plugins/datasource/tempo/traceql/situation.ts index d5709f4c8c9..3f1f6c8988f 100644 --- a/public/app/plugins/datasource/tempo/traceql/situation.ts +++ b/public/app/plugins/datasource/tempo/traceql/situation.ts @@ -86,7 +86,7 @@ type Path = Array<[Direction, NodeType[]]>; type Resolver = { path: NodeType[]; - fun: (node: SyntaxNode, text: string, pos: number) => SituationType | null; + fun: (node: SyntaxNode, text: string, pos: number, originalPos: number) => SituationType | null; }; function getErrorNode(tree: Tree, cursorPos: number): SyntaxNode | null { @@ -181,7 +181,7 @@ export function getSituation(text: string, offset: number): Situation | null { let situationType: SituationType | null = null; for (let resolver of RESOLVERS) { if (isPathMatch(resolver.path, ids)) { - situationType = resolver.fun(currentNode, text, shiftedOffset); + situationType = resolver.fun(currentNode, text, shiftedOffset, offset); } } @@ -191,7 +191,7 @@ export function getSituation(text: string, offset: number): Situation | null { const ERROR_NODE_ID = 0; const RESOLVERS: Resolver[] = [ - // Incomplete query cases + // Curson on error node cases { path: [ERROR_NODE_ID, AttributeField], fun: resolveAttribute, @@ -230,12 +230,10 @@ const RESOLVERS: Resolver[] = [ }; }, }, - // Valid query cases + // Curson on valid node cases (the whole query could contain errors nevertheless) { path: [FieldExpression], - fun: () => ({ - type: 'SPANSET_EXPRESSION_OPERATORS', - }), + fun: resolveSpanset, }, { path: [SpansetFilter], @@ -251,30 +249,53 @@ const RESOLVERS: Resolver[] = [ }, ]; -function resolveSpanset(node: SyntaxNode): SituationType { - const firstChild = walk(node, [ +const resolveAttributeCompletion = (node: SyntaxNode, text: string, pos: number): SituationType | void => { + // The user is completing an expression. We can take advantage of the fact that the Monaco editor is smart + // enough to automatically detect that there are some characters before the cursor and to take them into + // account when providing suggestions. + const endOfPathNode = walk(node, [['firstChild', [FieldExpression]]]); + if (endOfPathNode && text[pos - 1] !== ' ') { + const attributeFieldParent = walk(endOfPathNode, [['firstChild', [AttributeField]]]); + const attributeFieldParentText = attributeFieldParent ? getNodeText(attributeFieldParent, text) : ''; + const indexOfDot = attributeFieldParentText.indexOf('.'); + const attributeFieldUpToDot = attributeFieldParentText.slice(0, indexOfDot); + + return { + type: 'SPANSET_IN_NAME_SCOPE', + scope: attributeFieldUpToDot, + }; + } +}; + +function resolveSpanset(node: SyntaxNode, text: string, _: number, originalPos: number): SituationType { + const situation = resolveAttributeCompletion(node, text, originalPos); + if (situation) { + return situation; + } + + let endOfPathNode = walk(node, [ ['firstChild', [FieldExpression]], ['firstChild', [AttributeField]], ]); - if (firstChild) { + if (endOfPathNode) { return { type: 'SPANSET_EXPRESSION_OPERATORS', }; } - const lastFieldExpression1 = walk(node, [ + endOfPathNode = walk(node, [ ['lastChild', [FieldExpression]], ['lastChild', [FieldExpression]], ['lastChild', [Static]], ]); - if (lastFieldExpression1) { + if (endOfPathNode) { return { type: 'SPANFIELD_COMBINING_OPERATORS', }; } - const lastFieldExpression = walk(node, [['lastChild', [FieldExpression]]]); - if (lastFieldExpression) { + endOfPathNode = walk(node, [['lastChild', [FieldExpression]]]); + if (endOfPathNode) { return { type: 'SPANSET_EXPRESSION_OPERATORS', }; @@ -309,7 +330,12 @@ function resolveAttribute(node: SyntaxNode, text: string): SituationType { }; } -function resolveExpression(node: SyntaxNode, text: string): SituationType { +function resolveExpression(node: SyntaxNode, text: string, _: number, originalPos: number): SituationType { + const situation = resolveAttributeCompletion(node, text, originalPos); + if (situation) { + return situation; + } + if (node.prevSibling?.type.id === FieldOp) { let attributeField = node.prevSibling.prevSibling; if (attributeField) {