Skip to content

Commit e53d9bd

Browse files
committed
Fix editor marker cleanup for autocomplete
1 parent 5609e08 commit e53d9bd

20 files changed

Lines changed: 248 additions & 84 deletions

File tree

src/platform/packages/shared/kbn-esql-language/src/__tests__/commands/autocomplete.ts

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
import { uniq } from 'lodash';
1616
import type { LicenseType } from '@kbn/licensing-types';
1717
import type { EsqlFieldType } from '@kbn/esql-types';
18-
import { Parser } from '@elastic/esql';
1918
import type { ESQLAstAllCommands } from '@elastic/esql/types';
2019
import type {
2120
ICommandCallbacks,
@@ -45,7 +44,7 @@ import { FunctionDefinitionTypes } from '../../commands/definitions/types';
4544
import { mockContext, getMockCallbacks } from './context_fixtures';
4645
import { getSafeInsertText } from '../../commands/definitions/utils';
4746
import { timeUnitsToSuggest } from '../../commands/definitions/constants';
48-
import { correctQuerySyntax, findAstPosition } from '../../commands/definitions/utils/ast';
47+
import { findAutocompleteAstPosition } from '../../language/shared/parse_for_autocomplete_query';
4948
import { FUNCTIONS_TO_IGNORE } from '../../commands/registry/eval/autocomplete';
5049
import { attachReplacementRanges } from '../../language/autocomplete/utils/prefix_range';
5150

@@ -87,24 +86,20 @@ export const suggest = async (
8786
) => Promise<ISuggestionItem[]>,
8887
offset?: number
8988
): Promise<ISuggestionItem[]> => {
90-
const innerText = query.substring(0, offset ?? query.length);
91-
const correctedQuery = correctQuerySyntax(innerText);
92-
const { root } = Parser.parse(correctedQuery, { withFormatting: true });
93-
const headerConstruction = root?.header?.find((cmd) => cmd.name === commandName);
94-
9589
const cursorPosition = offset ?? query.length;
90+
const { innerText, root, command } = findAutocompleteAstPosition(query, cursorPosition);
91+
const headerConstruction = root?.header?.find((cmd) => cmd.name === commandName);
92+
const targetCommand = headerConstruction ?? command;
9693

97-
const command = headerConstruction ?? findAstPosition(root, cursorPosition).command;
98-
99-
if (!command) {
94+
if (!targetCommand) {
10095
throw new Error(`${commandName.toUpperCase()} command not found in the parsed query`);
10196
}
10297

10398
const contextWithRoot = { ...context, rootAst: root };
10499

105100
const suggestions = await autocomplete(
106101
query,
107-
command,
102+
targetCommand,
108103
mockCallbacks,
109104
contextWithRoot,
110105
cursorPosition

src/platform/packages/shared/kbn-esql-language/src/commands/definitions/utils/ast.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,27 @@ export function removeMarkerArgFromArgsList<T extends ESQLSingleAstItem | ESQLAs
9696
}
9797

9898
export function mapToNonMarkerNode(arg: ESQLAstItem): ESQLAstItem {
99-
return Array.isArray(arg) ? arg.filter(isNotMarkerNodeOrArray).map(mapToNonMarkerNode) : arg;
99+
if (Array.isArray(arg)) {
100+
return arg.filter(isNotMarkerNodeOrArray).map(mapToNonMarkerNode);
101+
}
102+
103+
if ('args' in arg) {
104+
return {
105+
...arg,
106+
args: arg.args.filter(isNotMarkerNodeOrArray).map(mapToNonMarkerNode) as typeof arg.args,
107+
} as typeof arg;
108+
}
109+
110+
if ('values' in arg) {
111+
return {
112+
...arg,
113+
values: arg.values
114+
.filter((value) => !isMarkerNode(value))
115+
.map((value) => mapToNonMarkerNode(value) as ESQLAstExpression) as typeof arg.values,
116+
} as typeof arg;
117+
}
118+
119+
return arg;
100120
}
101121

102122
function cleanMarkerNode(node: ESQLSingleAstItem | undefined): ESQLSingleAstItem | undefined {

src/platform/packages/shared/kbn-esql-language/src/commands/definitions/utils/autocomplete/expressions/operators/utils.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99

1010
import { isList } from '@elastic/esql';
1111
import type { ESQLSingleAstItem } from '@elastic/esql/types';
12-
import { isMarkerNode } from '../../../ast';
1312
import { getOperatorSuggestion } from '../../../operators';
1413
import type { ISuggestionItem } from '../../../../../registry/types';
1514
import { logicalOperators } from '../../../../all_operators';
@@ -34,11 +33,7 @@ export function endsWithIsOrIsNotToken(innerText: string): boolean {
3433
}
3534

3635
export function isOperandMissing(operand: ESQLSingleAstItem | undefined): boolean {
37-
return (
38-
!operand ||
39-
isMarkerNode(operand) ||
40-
(operand?.type === 'unknown' && operand?.incomplete === true)
41-
);
36+
return !operand || (operand?.type === 'unknown' && operand?.incomplete === true);
4237
}
4338

4439
/** Returns true if we should suggest opening a list for the right operand */

src/platform/packages/shared/kbn-esql-language/src/commands/definitions/utils/expressions.test.ts

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@
66
* your election, the "Elastic License 2.0", the "GNU Affero General Public
77
* License v3.0 only", or the "Server Side Public License, v 1".
88
*/
9-
import { Parser } from '@elastic/esql';
9+
import { isAssignment, Parser } from '@elastic/esql';
1010
import type { SupportedDataType } from '../types';
1111
import { FunctionDefinitionTypes } from '../types';
1212
import type { ESQLColumnData } from '../../registry/types';
1313
import { Location } from '../../registry/types';
14-
import { buildPartialMatcher, getExpressionType } from './expressions';
14+
import { buildPartialMatcher, getAssignmentExpressionRoot, getExpressionType } from './expressions';
1515
import { setTestFunctions } from './test_functions';
1616
import { TIME_SYSTEM_PARAMS } from './literals';
17+
import { getAutocompleteCursorContext } from '../../../language/shared/parse_for_autocomplete_query';
1718

1819
describe('buildPartialMatcher', () => {
1920
it('should build a partial matcher', () => {
@@ -458,3 +459,41 @@ describe('getExpressionType', () => {
458459
);
459460
});
460461
});
462+
463+
describe('getAssignmentExpressionRoot', () => {
464+
it('returns undefined for an incomplete assignment after autocomplete parsing cleanup', () => {
465+
const query = 'FROM employees | EVAL total = ';
466+
const { astContext } = getAutocompleteCursorContext(query, query.length);
467+
468+
if (astContext.type !== 'expression') {
469+
throw new Error(`Expected expression context for query: ${query}`);
470+
}
471+
472+
const assignment = astContext.command.args[astContext.command.args.length - 1];
473+
474+
if (!assignment || !isAssignment(assignment)) {
475+
throw new Error(`Expected assignment expression for query: ${query}`);
476+
}
477+
478+
expect(getAssignmentExpressionRoot(assignment)).toBeUndefined();
479+
});
480+
481+
it('returns the RHS root for a complete assignment after autocomplete parsing cleanup', () => {
482+
const query = 'FROM employees | EVAL total = salary';
483+
const { astContext } = getAutocompleteCursorContext(query, query.length);
484+
485+
if (astContext.type !== 'expression') {
486+
throw new Error(`Expected expression context for query: ${query}`);
487+
}
488+
489+
const assignment = astContext.command.args[astContext.command.args.length - 1];
490+
491+
if (!assignment || !isAssignment(assignment)) {
492+
throw new Error(`Expected assignment expression for query: ${query}`);
493+
}
494+
495+
const root = getAssignmentExpressionRoot(assignment);
496+
497+
expect(root).toMatchObject({ type: 'column', name: 'salary' });
498+
});
499+
});

src/platform/packages/shared/kbn-esql-language/src/commands/definitions/utils/expressions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ import { getColumnForASTNode } from './shared';
2424
import type { ESQLColumnData } from '../../registry/types';
2525
import { UnmappedFieldsStrategy } from '../../registry/types';
2626
import { TIME_SYSTEM_PARAMS } from './literals';
27-
import { isMarkerNode } from './ast';
2827
import { getUnmappedFieldType } from './settings';
2928
import { getPromqlFunctionDefinition } from './promql';
29+
import { isMarkerNode } from './ast';
3030

3131
// #region type detection
3232

src/platform/packages/shared/kbn-esql-language/src/commands/registry/completion/autocomplete.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import type {
1919
import { suggestForExpression } from '../../definitions/utils';
2020
import type { MapParameters } from '../../definitions/utils/autocomplete/map_expression';
2121
import { getCommandMapExpressionSuggestions } from '../../definitions/utils/autocomplete/map_expression';
22-
import { EDITOR_MARKER } from '../../definitions/constants';
2322
import {
2423
pipeCompleteItem,
2524
assignCompletionItem,
@@ -85,7 +84,7 @@ function getPosition(
8584
return { position: CompletionPosition.AFTER_COMMAND };
8685
}
8786

88-
const expressionRoot = prompt?.text !== EDITOR_MARKER ? prompt : undefined;
87+
const expressionRoot = prompt;
8988

9089
// (function, literal, or existing column) - handle as primaryExpression
9190
if (isFunctionExpression(expressionRoot) || isLiteral(prompt) || isExistingColumn) {

src/platform/packages/shared/kbn-esql-language/src/commands/registry/dissect/autocomplete.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,13 @@
88
*/
99
import { i18n } from '@kbn/i18n';
1010
import type { ESQLAstAllCommands } from '@elastic/esql/types';
11-
import { Parser } from '@elastic/esql';
1211
import { withAutoSuggest } from '../../definitions/utils/autocomplete/helpers';
1312
import type { ICommandCallbacks } from '../types';
1413
import { pipeCompleteItem, colonCompleteItem, semiColonCompleteItem } from '../complete_items';
1514
import { type ISuggestionItem, type ICommandContext } from '../types';
1615
import { buildConstantsDefinitions } from '../../definitions/utils/literals';
1716
import { ESQL_STRING_TYPES } from '../../definitions/types';
18-
import { correctQuerySyntax, findAstPosition } from '../../definitions/utils/ast';
17+
import { findAutocompleteAstPosition } from '../../../language/shared/parse_for_autocomplete_query';
1918

2019
const appendSeparatorCompletionItem: ISuggestionItem = withAutoSuggest({
2120
detail: i18n.translate('kbn-esql-language.esql.definitions.appendSeparatorDoc', {
@@ -39,9 +38,7 @@ export async function autocomplete(
3938
const commandArgs = command.args.filter((arg) => !Array.isArray(arg) && arg.type !== 'unknown');
4039

4140
// If cursor is inside a string literal, don't suggest anything
42-
const correctedQuery = correctQuerySyntax(innerText);
43-
const { root } = Parser.parse(correctedQuery, { withFormatting: true });
44-
const { node } = findAstPosition(root, innerText.length);
41+
const { node } = findAutocompleteAstPosition(query, cursorPosition);
4542

4643
if (node?.type === 'literal' && node.literalType === 'keyword') {
4744
return [];

src/platform/packages/shared/kbn-esql-language/src/commands/registry/fork/autocomplete.test.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,7 @@ import {
2929
ESQL_STRING_TYPES,
3030
ESQL_NUMBER_TYPES,
3131
} from '../../definitions/types';
32-
import { correctQuerySyntax, findAstPosition } from '../../definitions/utils/ast';
33-
import { Parser } from '@elastic/esql';
32+
import { findAutocompleteAstPosition } from '../../../language/shared/parse_for_autocomplete_query';
3433

3534
const allEvalFnsForWhere = getFunctionSignaturesByReturnType(Location.WHERE, 'any', {
3635
scalar: true,
@@ -428,11 +427,8 @@ describe('FORK Autocomplete', () => {
428427

429428
it('suggests pipe after complete subcommands', async () => {
430429
const assertSuggestsPipe = async (query: string) => {
431-
const correctedQuery = correctQuerySyntax(query);
432-
const { root } = Parser.parse(correctedQuery, { withFormatting: true });
433-
434430
const cursorPosition = query.length;
435-
const { command } = findAstPosition(root, cursorPosition);
431+
const { command } = findAutocompleteAstPosition(query, cursorPosition);
436432
if (!command) {
437433
throw new Error('Command not found in the parsed query');
438434
}

src/platform/packages/shared/kbn-esql-language/src/commands/registry/from/autocomplete.test.ts

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@ import {
1515
} from '../options/recommended_queries';
1616
import type { ICommandCallbacks } from '../types';
1717
import { autocomplete } from './autocomplete';
18-
import { correctQuerySyntax, findAstPosition } from '../../definitions/utils/ast';
19-
import { Parser } from '@elastic/esql';
18+
import { findAutocompleteAstPosition } from '../../../language/shared/parse_for_autocomplete_query';
2019

2120
const metadataFields = [...METADATA_FIELDS].sort();
2221

@@ -80,12 +79,8 @@ describe('FROM Autocomplete', () => {
8079
};
8180

8281
const suggest = async (query: string) => {
83-
const correctedQuery = correctQuerySyntax(query);
84-
const { root } = Parser.parse(correctedQuery, { withFormatting: true });
85-
8682
const cursorPosition = query.length;
87-
const { command } = findAstPosition(root, cursorPosition);
88-
83+
const { command } = findAutocompleteAstPosition(query, cursorPosition);
8984
return autocomplete(query, command!, mockCallbacks, mockContext, cursorPosition);
9085
};
9186

@@ -120,12 +115,8 @@ describe('FROM Autocomplete', () => {
120115

121116
test('suggests comma or pipe after complete index name', async () => {
122117
const suggest = async (query: string) => {
123-
const correctedQuery = correctQuerySyntax(query);
124-
const { root } = Parser.parse(correctedQuery, { withFormatting: true });
125-
126118
const cursorPosition = query.length;
127-
const { command } = findAstPosition(root, cursorPosition);
128-
119+
const { command } = findAutocompleteAstPosition(query, cursorPosition);
129120
return autocomplete(query, command!, mockCallbacks, mockContext, cursorPosition);
130121
};
131122
const suggestions = (await suggest('from index')).map((s) => s.text);
@@ -176,10 +167,8 @@ describe('FROM Autocomplete', () => {
176167
);
177168
// View names appear when typing (fragment "my_")
178169
const getSuggestions = async (query: string) => {
179-
const correctedQuery = correctQuerySyntax(query);
180-
const { root } = Parser.parse(correctedQuery, { withFormatting: true });
181170
const cursorPosition = query.length;
182-
const { command } = findAstPosition(root, cursorPosition);
171+
const { command } = findAutocompleteAstPosition(query, cursorPosition);
183172
return autocomplete(query, command!, mockCallbacks, contextWithViews, cursorPosition);
184173
};
185174
const suggestions = (await getSuggestions('FROM my_')).map((s) => s.text);

src/platform/packages/shared/kbn-esql-language/src/commands/registry/grok/autocomplete.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,13 @@
88
*/
99
import { i18n } from '@kbn/i18n';
1010
import type { ESQLAstAllCommands } from '@elastic/esql/types';
11-
import { Parser } from '@elastic/esql';
1211
import { withAutoSuggest } from '../../definitions/utils/autocomplete/helpers';
1312
import { commaCompleteItem, pipeCompleteItem } from '../complete_items';
1413
import type { ICommandCallbacks } from '../types';
1514
import type { ISuggestionItem, ICommandContext } from '../types';
1615
import { buildConstantsDefinitions } from '../../definitions/utils/literals';
1716
import { ESQL_STRING_TYPES } from '../../definitions/types';
18-
import { correctQuerySyntax, findAstPosition } from '../../definitions/utils/ast';
17+
import { findAutocompleteAstPosition } from '../../../language/shared/parse_for_autocomplete_query';
1918

2019
export async function autocomplete(
2120
query: string,
@@ -28,9 +27,7 @@ export async function autocomplete(
2827
const commandArgs = command.args.filter((arg) => !Array.isArray(arg) && arg.type !== 'unknown');
2928

3029
// If cursor is inside a string literal, don't suggest anything
31-
const correctedQuery = correctQuerySyntax(innerText);
32-
const { root } = Parser.parse(correctedQuery, { withFormatting: true });
33-
const { node } = findAstPosition(root, innerText.length);
30+
const { node } = findAutocompleteAstPosition(query, cursorPosition);
3431

3532
if (node?.type === 'literal' && node.literalType === 'keyword') {
3633
return [];

0 commit comments

Comments
 (0)