77 * License v3.0 only", or the "Server Side Public License, v 1".
88 */
99
10- import { isColumn , isIdentifier , isList , isOptionNode , isSource , Walker } from '@elastic/esql' ;
10+ import { isList , isOptionNode , PromQLParser , Walker } from '@elastic/esql' ;
11+ import type { PromQLAstNode , PromQLAstQueryExpression } from '@elastic/esql' ;
1112import type {
1213 ESQLFunction ,
1314 ESQLSingleAstItem ,
14- ESQLAstExpression ,
1515 ESQLAstItem ,
1616 ESQLCommandOption ,
17- ESQLAstAllCommands ,
1817 ESQLAstHeaderCommand ,
1918 ESQLAstQueryExpression ,
2019} from '@elastic/esql/types' ;
2120import { EDITOR_MARKER } from '../constants' ;
22- import { endsWithComma } from './regex' ;
21+ import { endsWithComma , endsWithWhitespace } from './regex' ;
2322
24- export function isMarkerNode ( node : ESQLAstItem | undefined ) : boolean {
25- if ( Array . isArray ( node ) ) {
26- return false ;
27- }
23+ const ENDS_WITH_BINARY_OPERATOR_REGEX =
24+ / (?: \+ | \/ | = = | > = | > | < = | < | : | % | \* | - | ! = | = | \b (?: i n | l i k e | n o t i n | n o t l i k e | n o t r l i k e | r l i k e | a n d | o r | n o t | a s ) \b ) \s + $ / i;
25+ const ENDS_WITH_CASTING_OPERATOR_REGEX = / : : \s * $ / i;
2826
29- return Boolean (
30- node &&
31- ( isColumn ( node ) || isIdentifier ( node ) || isSource ( node ) ) &&
32- node . name . endsWith ( EDITOR_MARKER )
33- ) ;
27+ export function isMarkerNode ( node : ESQLAstItem | PromQLAstNode | undefined ) : boolean {
28+ return Boolean ( node && ! Array . isArray ( node ) && node . name ?. endsWith ( EDITOR_MARKER ) ) ;
3429}
3530
3631function findCommand ( ast : ESQLAstQueryExpression , offset : number ) {
@@ -65,81 +60,37 @@ function findHeaderCommand(
6560 return targetHeader . incomplete ? targetHeader : undefined ;
6661}
6762
68- export function isNotMarkerNodeOrArray ( arg : ESQLAstItem ) {
69- return Array . isArray ( arg ) || ! isMarkerNode ( arg ) ;
70- }
71-
72- const removeMarkerNode = ( node : ESQLAstExpression ) => {
73- Walker . walk ( node , {
74- visitAny : ( current ) => {
75- if ( 'args' in current ) {
76- current . args = current . args . filter ( ( n ) => ! isMarkerNode ( n ) ) ;
77- } else if ( 'values' in current ) {
78- current . values = current . values . filter ( ( n ) => ! isMarkerNode ( n ) ) ;
79- }
80- } ,
81- } ) ;
82- } ;
83-
84- function removeMarkerArgFromArgsList < T extends ESQLSingleAstItem | ESQLAstAllCommands > (
85- node : T | undefined
86- ) {
87- if ( ! node ) {
88- return ;
63+ // Removes parser-only autocomplete markers while preserving parser locations for cursor math.
64+ export function removeAutocompleteMarkers < T > ( value : T ) : T {
65+ if ( Array . isArray ( value ) ) {
66+ return value
67+ . filter ( ( item ) => ! isMarkerNode ( item ) )
68+ . map ( ( item ) => removeAutocompleteMarkers ( item ) ) as T ;
8969 }
90- if ( node . type === 'command' || node . type === 'option' || node . type === 'function' ) {
91- const cleanedNode = {
92- ...node ,
93- text : node . text . replace ( EDITOR_MARKER , '' ) ,
94- args : node . args . filter ( isNotMarkerNodeOrArray ) . map ( mapToNonMarkerNode ) ,
95- } ;
96-
97- if ( cleanedNode . type === 'command' && 'expression' in cleanedNode && cleanedNode . expression ) {
98- cleanedNode . expression = isMarkerNode ( cleanedNode . expression )
99- ? undefined
100- : ( mapToNonMarkerNode ( cleanedNode . expression ) as ESQLAstExpression ) ;
101- }
10270
103- return cleanedNode ;
71+ if ( ! value || typeof value !== 'object' ) {
72+ return value ;
10473 }
105- return node ;
106- }
10774
108- export function mapToNonMarkerNode ( arg : ESQLAstItem ) : ESQLAstItem {
109- if ( Array . isArray ( arg ) ) {
110- return arg . filter ( isNotMarkerNodeOrArray ) . map ( mapToNonMarkerNode ) ;
75+ if ( isMarkerNode ( value as ESQLAstItem | PromQLAstNode ) ) {
76+ return undefined as T ;
11177 }
11278
113- if ( 'args' in arg ) {
114- return {
115- ...arg ,
116- text : arg . text . replace ( EDITOR_MARKER , '' ) ,
117- args : arg . args . filter ( isNotMarkerNodeOrArray ) . map ( mapToNonMarkerNode ) as typeof arg . args ,
118- } as typeof arg ;
119- }
79+ const cleanedValue = { ...value } ;
12080
121- if ( 'values' in arg ) {
122- return {
123- ...arg ,
124- text : arg . text . replace ( EDITOR_MARKER , '' ) ,
125- values : arg . values
126- . filter ( ( value ) => ! isMarkerNode ( value ) )
127- . map ( ( value ) => mapToNonMarkerNode ( value ) as ESQLAstExpression ) as typeof arg . values ,
128- } as typeof arg ;
81+ for ( const [ key , child ] of Object . entries ( value ) ) {
82+ cleanedValue [ key as keyof typeof cleanedValue ] = removeAutocompleteMarkers ( child ) ;
12983 }
13084
131- if ( 'text' in arg && typeof arg . text === 'string' && arg . text . includes ( EDITOR_MARKER ) ) {
132- return {
133- ...arg ,
134- text : arg . text . replace ( EDITOR_MARKER , '' ) ,
135- } as typeof arg ;
85+ if (
86+ 'text' in cleanedValue &&
87+ typeof cleanedValue . text === 'string' &&
88+ cleanedValue . text . includes ( EDITOR_MARKER )
89+ ) {
90+ cleanedValue . text = cleanedValue . text . replace ( EDITOR_MARKER , '' ) ;
13691 }
13792
138- return arg ;
139- }
140-
141- function cleanMarkerNode ( node : ESQLSingleAstItem | undefined ) : ESQLSingleAstItem | undefined {
142- return isMarkerNode ( node ) ? undefined : node ;
93+ return cleanedValue ;
14394}
14495
14596function findOption ( nodes : ESQLAstItem [ ] , offset : number ) : ESQLCommandOption | undefined {
@@ -174,7 +125,7 @@ export function findAstPosition(ast: ESQLAstQueryExpression, offset: number) {
174125
175126 Walker . walk ( command , {
176127 visitSource : ( _node , parent , walker ) => {
177- if ( _node . location . max >= offset && _node . text !== EDITOR_MARKER ) {
128+ if ( _node . location . max >= offset ) {
178129 node = _node as ESQLSingleAstItem ;
179130 walker . abort ( ) ;
180131 }
@@ -188,23 +139,17 @@ export function findAstPosition(ast: ESQLAstQueryExpression, offset: number) {
188139 containingFunction = _node ;
189140 }
190141
191- if (
192- _node . location . max >= offset &&
193- _node . text !== EDITOR_MARKER &&
194- ( ! isList ( _node ) || _node . subtype !== 'tuple' )
195- ) {
142+ if ( _node . location . max >= offset && ( ! isList ( _node ) || _node . subtype !== 'tuple' ) ) {
196143 node = _node as ESQLSingleAstItem ;
197144 }
198145 } ,
199146 } ) ;
200147
201- if ( node ) removeMarkerNode ( node ) ;
202-
203148 return {
204- command : removeMarkerArgFromArgsList ( command ) ! ,
205- containingFunction : removeMarkerArgFromArgsList ( containingFunction ) ,
206- option : removeMarkerArgFromArgsList ( findOption ( command . args , offset ) ) ,
207- node : removeMarkerArgFromArgsList ( cleanMarkerNode ( node ) ) ,
149+ command,
150+ containingFunction,
151+ option : findOption ( command . args , offset ) ,
152+ node,
208153 } ;
209154}
210155
@@ -283,6 +228,18 @@ export function getBracketsToClose(text: string) {
283228 return stack . reverse ( ) . map ( ( bracket ) => pairs [ bracket ] ) ;
284229}
285230
231+ export function addAutocompleteMarker ( query : string ) {
232+ if (
233+ ENDS_WITH_BINARY_OPERATOR_REGEX . test ( query ) ||
234+ ENDS_WITH_CASTING_OPERATOR_REGEX . test ( query ) ||
235+ ( endsWithComma ( query ) && endsWithWhitespace ( query ) )
236+ ) {
237+ return `${ query } ${ EDITOR_MARKER } ` ;
238+ }
239+
240+ return query ;
241+ }
242+
286243/**
287244 * This function attempts to correct the syntax of a partial query to make it valid.
288245 *
@@ -293,27 +250,9 @@ export function getBracketsToClose(text: string) {
293250 * @param context
294251 * @returns
295252 */
296- export function correctQuerySyntax ( _query : string ) {
297- let query = _query ;
253+ export function correctQuerySyntax ( query : string ) {
298254 // check if all brackets are closed, otherwise close them
299- const bracketsToAppend = getBracketsToClose ( query ) ;
300-
301- const endsWithBinaryOperatorRegex =
302- / (?: \+ | \/ | = = | > = | > | < = | < | : | % | \* | - | ! = | = | \b (?: i n | l i k e | n o t i n | n o t l i k e | n o t r l i k e | r l i k e | a n d | o r | n o t | a s ) \b ) \s + $ / i;
303- const endsWithCastingOperatorRegex = / : : \s * $ / i;
304- const endsWithCommaRegex = / , \s + $ / ;
305-
306- if (
307- endsWithBinaryOperatorRegex . test ( query ) ||
308- endsWithCastingOperatorRegex . test ( query ) ||
309- endsWithCommaRegex . test ( query )
310- ) {
311- query += ` ${ EDITOR_MARKER } ` ;
312- }
313-
314- query += bracketsToAppend . join ( '' ) ;
315-
316- return query ;
255+ return query + getBracketsToClose ( query ) . join ( '' ) ;
317256}
318257
319258const PROMQL_TRAILING_COLON_REGEX = / : \s * $ / ;
@@ -336,6 +275,19 @@ export function correctPromqlQuerySyntax(input: string): string {
336275 return query + getPromqlBracketsToClose ( query ) ;
337276}
338277
278+ export function parsePromqlAutocompleteQuery ( query : string ) : {
279+ correctedQuery : string ;
280+ root : PromQLAstQueryExpression ;
281+ } {
282+ const correctedQuery = correctPromqlQuerySyntax ( query ) ;
283+ const { root } = PromQLParser . parse ( correctedQuery ) ;
284+
285+ return {
286+ correctedQuery,
287+ root : removeAutocompleteMarkers ( root ) ,
288+ } ;
289+ }
290+
339291function getPromqlBracketsToClose ( text : string ) : string {
340292 const esqlBrackets = getBracketsToClose ( text ) . join ( '' ) ;
341293 const promqlBraces = getPromqlBracesToClose ( text ) ;
0 commit comments