diff --git a/public/app/plugins/datasource/tempo/traceql/autocomplete.test.ts b/public/app/plugins/datasource/tempo/traceql/autocomplete.test.ts index bba93e29820..347027f5f35 100644 --- a/public/app/plugins/datasource/tempo/traceql/autocomplete.test.ts +++ b/public/app/plugins/datasource/tempo/traceql/autocomplete.test.ts @@ -342,6 +342,27 @@ describe('CompletionProvider', () => { [...scopes, ...intrinsics].map((s) => expect.objectContaining({ label: s })) ); }); + + it.each([ + ['{span.ht', 8], + ['{span.http', 10], + ['{span.http.', 11], + ['{span.http.status', 17], + ])( + 'suggests attributes when containing trigger characters and missing `}`- %s, %i', + async (input: string, offset: number) => { + const { provider, model } = setup(input, offset, undefined, [ + { + name: 'span', + tags: ['http.status_code'], + }, + ]); + const result = await provider.provideCompletionItems(model, emptyPosition); + expect((result! as monacoTypes.languages.CompletionList).suggestions).toEqual([ + expect.objectContaining({ label: 'http.status_code', insertText: 'http.status_code' }), + ]); + } + ); }); function setup(value: string, offset: number, tagsV1?: string[], tagsV2?: Scope[]) { diff --git a/public/app/plugins/datasource/tempo/traceql/situation.ts b/public/app/plugins/datasource/tempo/traceql/situation.ts index 3f1f6c8988f..9102bd742a0 100644 --- a/public/app/plugins/datasource/tempo/traceql/situation.ts +++ b/public/app/plugins/datasource/tempo/traceql/situation.ts @@ -202,9 +202,7 @@ const RESOLVERS: Resolver[] = [ }, { path: [ERROR_NODE_ID, SpansetFilter], - fun: () => ({ - type: 'SPANSET_EXPRESSION_OPERATORS_WITH_MISSING_CLOSED_BRACE', - }), + fun: resolveSpansetWithNoClosedBrace, }, { path: [ERROR_NODE_ID, Aggregate], @@ -253,16 +251,35 @@ const resolveAttributeCompletion = (node: SyntaxNode, text: string, pos: number) // 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 getAttributeFieldUpToDot = (node: SyntaxNode) => { + const attributeFieldParent = walk(node, [['firstChild', [AttributeField]]]); const attributeFieldParentText = attributeFieldParent ? getNodeText(attributeFieldParent, text) : ''; const indexOfDot = attributeFieldParentText.indexOf('.'); - const attributeFieldUpToDot = attributeFieldParentText.slice(0, indexOfDot); + return attributeFieldParentText.slice(0, indexOfDot); + }; + // If there is a space, for sure the attribute is completed and no suggestions to complete it should be provided + if (text[pos - 1] === ' ') { + return; + } + + const endOfPathNode = walk(node, [['firstChild', [FieldExpression]]]); + if (endOfPathNode) { return { type: 'SPANSET_IN_NAME_SCOPE', - scope: attributeFieldUpToDot, + scope: getAttributeFieldUpToDot(endOfPathNode), + }; + } + + const endOfPathNode2 = walk(node, [ + ['parent', [SpansetFilter]], + ['firstChild', [FieldExpression]], + ]); + // In this case, we also need to check the character at `pos` + if (endOfPathNode2 && text[pos] !== ' ') { + return { + type: 'SPANSET_IN_NAME_SCOPE', + scope: getAttributeFieldUpToDot(endOfPathNode2), }; } }; @@ -415,3 +432,14 @@ function resolveSpansetPipeline(node: SyntaxNode, _1: string, _2: number): Situa type: 'NEW_SPANSET', }; } + +function resolveSpansetWithNoClosedBrace(node: SyntaxNode, text: string, originalPos: number): SituationType { + const situation = resolveAttributeCompletion(node, text, originalPos); + if (situation) { + return situation; + } + + return { + type: 'SPANSET_EXPRESSION_OPERATORS_WITH_MISSING_CLOSED_BRACE', + }; +}