Skip to content

Commit fe80f76

Browse files
ElenaStoevakibanamachinestratoulajloleysens
authored
[Console] Add ESQL autocomplete support (#219980)
Closes #208450 ## Summary This PR adds ESQL autocomplete support to Console. The ESQL suggestions are only displayed inside triple-quotes queries (`"query": """...`). **How to test:** 1. Open Kibana and load some data (e.g. the sample data sets). 2. Navigate to Dev Tools -> Console and type in the following query: ``` POST _query { "query": """ """ } ``` 3. Verify that ESQL suggestions are displayed inside the triple quotes and work as in the ESQL editor in Discover. Example ESQL query: `FROM kibana_sample_data_ecommerce | WHERE order_date >= ?_tstart AND order_date <= ?_tend | LIMIT 10` 4. Verify that no ESQL suggestions are displayed outside triple-quotes queries 5. Verify that no Console suggestions are displayed inside triple-quotes queries 6. Verify that the functionality also works in embeddable Console. https://github.com/user-attachments/assets/63f1b82d-50a2-4af4-b5ad-812eab9a55a5 ## Release notes These changes add autocompletion for ESQL query requests in Console. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Stratoula Kalafateli <efstratia.kalafateli@elastic.co> Co-authored-by: Jean-Louis Leysens <jloleysens@gmail.com>
1 parent 8572e3d commit fe80f76

30 files changed

Lines changed: 468 additions & 131 deletions

File tree

src/platform/packages/private/kbn-esql-editor/src/helpers.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ export const clearCacheWhenOld = (cache: MapCache, esqlQuery: string) => {
231231
}
232232
};
233233

234-
const getIntegrations = async (core: CoreStart) => {
234+
const getIntegrations = async (core: Pick<CoreStart, 'application' | 'http'>) => {
235235
const fleetCapabilities = core.application.capabilities.fleet;
236236
if (!fleetCapabilities?.read) {
237237
return [];
@@ -263,7 +263,7 @@ const getIntegrations = async (core: CoreStart) => {
263263

264264
export const getESQLSources = async (
265265
dataViews: DataViewsPublicPluginStart,
266-
core: CoreStart,
266+
core: Pick<CoreStart, 'application' | 'http'>,
267267
areRemoteIndicesAvailable: boolean
268268
) => {
269269
const [remoteIndices, localIndices, integrations] = await Promise.all([

src/platform/packages/shared/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,9 @@ export async function suggest(
135135
const visibleSources = sources.filter((source) => !source.hidden);
136136
if (visibleSources.find((source) => source.name.startsWith('logs'))) {
137137
fromCommand = 'FROM logs*';
138-
} else fromCommand = `FROM ${visibleSources[0].name}`;
138+
} else if (visibleSources.length) {
139+
fromCommand = `FROM ${visibleSources[0].name}`;
140+
}
139141

140142
const { getFieldsByType: getFieldsByTypeEmptyState } = getFieldsByTypeRetriever(
141143
fromCommand,

src/platform/packages/shared/kbn-monaco/src/languages/console/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export {
2121
ConsoleOutputLang,
2222
CONSOLE_THEME_ID,
2323
CONSOLE_OUTPUT_THEME_ID,
24+
CONSOLE_TRIGGER_CHARS,
2425
} from './language';
2526
export { ConsoleParsedRequestsProvider } from './console_parsed_requests_provider';
2627

src/platform/packages/shared/kbn-monaco/src/languages/console/language.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,17 @@
77
* License v3.0 only", or the "Server Side Public License, v 1".
88
*/
99

10+
import { type ESQLCallbacks, suggest } from '@kbn/esql-validation-autocomplete';
11+
import { MutableRefObject } from 'react';
1012
import { setupConsoleErrorsProvider } from './console_errors_provider';
1113
import { ConsoleWorkerProxyService } from './console_worker_proxy';
1214
import { monaco } from '../../monaco_imports';
1315
import { CONSOLE_LANG_ID, CONSOLE_OUTPUT_LANG_ID } from './constants';
16+
import { ESQL_AUTOCOMPLETE_TRIGGER_CHARS } from '../esql';
17+
import { wrapAsMonacoSuggestions } from '../esql/lib/converters/suggestions';
1418
import { ConsoleParsedRequestsProvider } from './console_parsed_requests_provider';
1519
import { buildConsoleTheme } from './theme';
20+
import { isInsideTripleQuotes } from './utils';
1621
import type { LangModuleType } from '../../types';
1722

1823
const workerProxyService = new ConsoleWorkerProxyService();
@@ -25,6 +30,8 @@ import {
2530
} from './lexer_rules';
2631
import { foldingRangeProvider } from './folding_range_provider';
2732

33+
export const CONSOLE_TRIGGER_CHARS = ['/', '.', '_', ',', '?', '=', '&', '"'];
34+
2835
/**
2936
* @description This language definition is used for the console input panel
3037
*/
@@ -38,6 +45,43 @@ export const ConsoleLang: LangModuleType = {
3845
setupConsoleErrorsProvider(workerProxyService);
3946
},
4047
languageThemeResolver: buildConsoleTheme,
48+
getSuggestionProvider: (
49+
esqlCallbacks: Pick<ESQLCallbacks, 'getSources' | 'getPolicies'>,
50+
actionsProvider: MutableRefObject<any>
51+
): monaco.languages.CompletionItemProvider => {
52+
return {
53+
// force suggestions when these characters are used
54+
triggerCharacters: [...CONSOLE_TRIGGER_CHARS, ...ESQL_AUTOCOMPLETE_TRIGGER_CHARS],
55+
provideCompletionItems: async (
56+
model: monaco.editor.ITextModel,
57+
position: monaco.Position,
58+
context: monaco.languages.CompletionContext
59+
) => {
60+
const fullText = model.getValue();
61+
const cursorOffset = model.getOffsetAt(position);
62+
const textBeforeCursor = fullText.slice(0, cursorOffset);
63+
const { insideQuery } = isInsideTripleQuotes(textBeforeCursor);
64+
if (esqlCallbacks && insideQuery) {
65+
const queryStartOffset = textBeforeCursor.lastIndexOf('"""') + 3;
66+
const queryText = textBeforeCursor.slice(queryStartOffset, cursorOffset);
67+
const esqlSuggestions = await suggest(
68+
queryText,
69+
cursorOffset - queryStartOffset,
70+
context,
71+
esqlCallbacks
72+
);
73+
return {
74+
suggestions: wrapAsMonacoSuggestions(esqlSuggestions, queryText, false),
75+
};
76+
} else if (actionsProvider.current) {
77+
return actionsProvider.current?.provideCompletionItems(model, position, context);
78+
}
79+
return {
80+
suggestions: [],
81+
};
82+
},
83+
};
84+
},
4185
};
4286

4387
/**
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
import { isInsideTripleQuotes } from './autocomplete_utils';
11+
12+
describe('autocomplete_utils', () => {
13+
describe('isInsideTripleQuotes', () => {
14+
it('should return false for both flags for an empty string', () => {
15+
expect(isInsideTripleQuotes('')).toEqual({
16+
insideTripleQuotes: false,
17+
insideQuery: false,
18+
});
19+
});
20+
21+
it('should return false for both flags for a request without triple quotes', () => {
22+
const request = `POST _search\n{\n "query": {\n "match": {\n "message": "hello world"\n }\n }\n}`;
23+
expect(isInsideTripleQuotes(request)).toEqual({
24+
insideTripleQuotes: false,
25+
insideQuery: false,
26+
});
27+
});
28+
29+
it('should return true for insideTripleQuotes and false for insideQuery if triple quotes are not in query', () => {
30+
const request = `POST _ingest/pipeline/_simulate\n{\n "pipeline": {\n "processors": [\n {\n "script": {\n "source":\n """\n for (field in params['fields']){\n if (!$(field, '').isEmpty()){\n`;
31+
expect(isInsideTripleQuotes(request)).toEqual({
32+
insideTripleQuotes: true,
33+
insideQuery: false,
34+
});
35+
});
36+
37+
it('should return false for both flags if triple-quoted string is properly closed', () => {
38+
const request = `POST _ingest/pipeline/_simulate\n{\n "pipeline": {\n "processors": [\n {\n "script": {\n "source":\n """\n return 'hello';\n """\n }\n }\n ]\n }\n}`;
39+
expect(isInsideTripleQuotes(request)).toEqual({
40+
insideTripleQuotes: false,
41+
insideQuery: false,
42+
});
43+
});
44+
45+
it('should return true for both flags if inside triple quotes and inside a "query" field', () => {
46+
const request = `POST _query\n{\n "query": """FROM test `;
47+
expect(isInsideTripleQuotes(request)).toEqual({
48+
insideTripleQuotes: true,
49+
insideQuery: true,
50+
});
51+
});
52+
});
53+
});
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
/**
11+
* This function determines whether the given text ends with unclosed triple quotes
12+
* and whether it ends with an unclosed triple-quotes query ("query": """...)
13+
* @param text The text up to the current position
14+
*/
15+
export const isInsideTripleQuotes = (
16+
text: string
17+
): { insideTripleQuotes: boolean; insideQuery: boolean } => {
18+
let insideTripleQuotes = false;
19+
let isCurrentTripleQuoteQuery = false;
20+
let i = 0;
21+
22+
while (i < text.length) {
23+
if (text.startsWith('"""', i)) {
24+
insideTripleQuotes = !insideTripleQuotes;
25+
if (insideTripleQuotes) {
26+
isCurrentTripleQuoteQuery = /.*"query"\s*:\s*/.test(text.slice(0, i));
27+
}
28+
i += 3; // Skip the triple quotes
29+
} else {
30+
i++;
31+
}
32+
}
33+
34+
return { insideTripleQuotes, insideQuery: insideTripleQuotes && isCurrentTripleQuoteQuery };
35+
};
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
export { isInsideTripleQuotes } from './autocomplete_utils';

src/platform/packages/shared/kbn-monaco/src/languages/esql/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@
88
*/
99

1010
export { ESQL_LANG_ID, ESQL_DARK_THEME_ID, ESQL_LIGHT_THEME_ID } from './lib/constants';
11-
export { ESQLLang } from './language';
11+
export { ESQLLang, ESQL_AUTOCOMPLETE_TRIGGER_CHARS } from './language';

src/platform/packages/shared/kbn-monaco/src/languages/esql/language.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ const removeKeywordSuffix = (name: string) => {
2828
return name.endsWith('.keyword') ? name.slice(0, -8) : name;
2929
};
3030

31+
export const ESQL_AUTOCOMPLETE_TRIGGER_CHARS = ['(', ' ', '[', '?'];
32+
3133
export const ESQLLang: CustomLangModuleType<ESQLCallbacks> = {
3234
ID: ESQL_LANG_ID,
3335
async onLanguage() {
@@ -78,7 +80,7 @@ export const ESQLLang: CustomLangModuleType<ESQLCallbacks> = {
7880
},
7981
getSuggestionProvider: (callbacks?: ESQLCallbacks): monaco.languages.CompletionItemProvider => {
8082
return {
81-
triggerCharacters: ['(', ' ', '[', '?'],
83+
triggerCharacters: ESQL_AUTOCOMPLETE_TRIGGER_CHARS,
8284
async provideCompletionItems(
8385
model: monaco.editor.ITextModel,
8486
position: monaco.Position,

src/platform/packages/shared/kbn-monaco/src/languages/esql/lib/converters/suggestions.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ import { offsetRangeToMonacoRange } from '../shared/utils';
1414

1515
export function wrapAsMonacoSuggestions(
1616
suggestions: SuggestionRawDefinition[],
17-
fullText: string
17+
fullText: string,
18+
defineRange: boolean = true
1819
): MonacoAutocompleteCommandDefinition[] {
1920
return suggestions.map<MonacoAutocompleteCommandDefinition>(
2021
({
@@ -44,7 +45,10 @@ export function wrapAsMonacoSuggestions(
4445
insertTextRules: asSnippet
4546
? monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet
4647
: undefined,
47-
range: rangeToReplace ? offsetRangeToMonacoRange(fullText, rangeToReplace) : undefined,
48+
range:
49+
rangeToReplace && defineRange
50+
? offsetRangeToMonacoRange(fullText, rangeToReplace)
51+
: undefined,
4852
};
4953
return monacoSuggestion;
5054
}

0 commit comments

Comments
 (0)