Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,34 @@ describe('esql query helpers', () => {
} as monaco.Position);
expect(values).toEqual(undefined);
});

it('should return undefined if the query has a questionmark at the last position', () => {
const queryString = 'FROM my_index | STATS COUNT() BY ?';
const values = getValuesFromQueryField(queryString, {
lineNumber: 1,
column: 34,
} as monaco.Position);
expect(values).toEqual(undefined);
});

it('should return undefined if the query has a questionmark at the second last position', () => {
const queryString = 'FROM my_index | STATS PERCENTILE(bytes, ?)';
const values = getValuesFromQueryField(queryString, {
lineNumber: 1,
column: 42,
} as monaco.Position);
expect(values).toEqual(undefined);
});

it('should return undefined if the query has a questionmark at the last cursor position', () => {
const queryString =
'FROM my_index | STATS COUNT() BY BUCKET(@timestamp, ?, ?_tstart, ?_tend)';
const values = getValuesFromQueryField(queryString, {
lineNumber: 1,
column: 52,
} as monaco.Position);
expect(values).toEqual(undefined);
});
});

describe('fixESQLQueryWithVariables', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ export const mapVariableToColumn = (
return columns;
};

const getQueryUpToCursor = (queryString: string, cursorPosition?: monaco.Position) => {
export const getQueryUpToCursor = (queryString: string, cursorPosition?: monaco.Position) => {
const lines = queryString.split('\n');
const lineNumber = cursorPosition?.lineNumber ?? lines.length;
const column = cursorPosition?.column ?? lines[lineNumber - 1].length;
Expand All @@ -210,9 +210,24 @@ const getQueryUpToCursor = (queryString: string, cursorPosition?: monaco.Positio
return previousLines + '\n' + currentLine;
};

const hasQuestionMarkAtEndOrSecondLastPosition = (queryString: string) => {
if (typeof queryString !== 'string' || queryString.length === 0) {
return false;
}

const lastChar = queryString.slice(-1);
const secondLastChar = queryString.slice(-2, -1);

return lastChar === '?' || secondLastChar === '?';
};

export const getValuesFromQueryField = (queryString: string, cursorPosition?: monaco.Position) => {
const queryInCursorPosition = getQueryUpToCursor(queryString, cursorPosition);

if (hasQuestionMarkAtEndOrSecondLastPosition(queryInCursorPosition)) {
return undefined;
}

const validQuery = `${queryInCursorPosition} ""`;
const { root } = parse(validQuery);
const lastCommand = root.commands[root.commands.length - 1];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,38 @@ import { setup } from './helpers';

describe('autocomplete.suggest', () => {
describe('LIMIT <number>', () => {
it('suggests numbers', async () => {
test('suggests numbers', async () => {
const { assertSuggestions } = await setup();
assertSuggestions('from a | limit /', ['10 ', '100 ', '1000 ']);
assertSuggestions('from a | limit /', ['10 ', '100 ', '1000 '], { triggerCharacter: ' ' });
});

it('suggests pipe after number', async () => {
test('suggests pipe after number', async () => {
const { assertSuggestions } = await setup();
assertSuggestions('from a | limit 4 /', ['| ']);
});
});

describe('create control suggestion', () => {
test('suggests `Create control` option if questionmark is typed', async () => {
const { suggest } = await setup();

const suggestions = await suggest('FROM a | LIMIT ?/', {
callbacks: {
canSuggestVariables: () => true,
getVariables: () => [],
getColumnsFor: () => Promise.resolve([{ name: 'agent.name', type: 'keyword' }]),
},
});

expect(suggestions).toContainEqual({
label: 'Create control',
text: '',
kind: 'Issue',
detail: 'Click to create',
command: { id: 'esql.control.values.create', title: 'Click to create' },
sortText: '1',
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,25 @@ describe('autocomplete.suggest', () => {
sortText: '1A',
});
});

test('suggests `Create control` option when ? is being typed', async () => {
const suggestions = await suggest('FROM a | STATS PERCENTILE(bytes, ?/)', {
callbacks: {
canSuggestVariables: () => true,
getVariables: () => [],
getColumnsFor: () => Promise.resolve([{ name: 'bytes', type: 'double' }]),
},
});

expect(suggestions).toContainEqual({
label: 'Create control',
text: '',
kind: 'Issue',
detail: 'Click to create',
command: { id: 'esql.control.values.create', title: 'Click to create' },
sortText: '1',
});
});
});
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,27 @@ describe('WHERE <expression>', () => {
rangeToReplace: { start: 30, end: 30 },
});
});

test('suggests `Create control` option when a questionmark is typed', async () => {
const { suggest } = await setup();

const suggestions = await suggest('FROM a | WHERE agent.name == ?/', {
callbacks: {
canSuggestVariables: () => true,
getVariables: () => [],
getColumnsFor: () => Promise.resolve([{ name: 'agent.name', type: 'keyword' }]),
},
});

expect(suggestions).toContainEqual({
label: 'Create control',
text: '',
kind: 'Issue',
detail: 'Click to create',
command: { id: 'esql.control.values.create', title: 'Click to create' },
sortText: '1',
});
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
type ESQLFunction,
type ESQLSingleAstItem,
} from '@kbn/esql-ast';
import type { ESQLControlVariable } from '@kbn/esql-types';
import { type ESQLControlVariable, ESQLVariableType } from '@kbn/esql-types';
import { isNumericType } from '../shared/esql_types';
import type { EditorContext, ItemKind, SuggestionRawDefinition, GetColumnsByTypeFn } from './types';
import {
Expand Down Expand Up @@ -49,6 +49,7 @@ import {
buildValueDefinitions,
getDateLiterals,
buildFieldsDefinitionsWithMetadata,
getControlSuggestionIfSupported,
} from './factories';
import { EDITOR_MARKER, FULL_TEXT_SEARCH_FUNCTIONS } from '../shared/constants';
import { getAstContext } from '../shared/context';
Expand Down Expand Up @@ -107,7 +108,8 @@ export async function suggest(

const { getFieldsByType, getFieldsMap } = getFieldsByTypeRetriever(
queryForFields.replace(EDITOR_MARKER, ''),
resourceRetriever
resourceRetriever,
innerText
);
const supportsControls = resourceRetriever?.canSuggestVariables?.() ?? false;
const getVariables = resourceRetriever?.getVariables;
Expand All @@ -131,7 +133,8 @@ export async function suggest(

const { getFieldsByType: getFieldsByTypeEmptyState } = getFieldsByTypeRetriever(
fromCommand,
resourceRetriever
resourceRetriever,
innerText
);
recommendedQueriesSuggestions.push(
...(await getRecommendedQueriesSuggestions(getFieldsByTypeEmptyState, fromCommand))
Expand All @@ -144,8 +147,21 @@ export async function suggest(
return suggestions.filter((def) => !isSourceCommand(def));
}

// ToDo: Reconsider where it belongs when this is resolved https://github.com/elastic/kibana/issues/216492
const lastCharacterTyped = innerText[innerText.length - 1];
let controlSuggestions: SuggestionRawDefinition[] = [];
if (lastCharacterTyped === '?') {
controlSuggestions = getControlSuggestionIfSupported(
Boolean(supportsControls),
ESQLVariableType.VALUES,
getVariables
);

return controlSuggestions;
}

if (astContext.type === 'expression') {
return getSuggestionsWithinCommandExpression(
const commandsSpecificSuggestions = await getSuggestionsWithinCommandExpression(
innerText,
ast,
astContext,
Expand All @@ -159,9 +175,10 @@ export async function suggest(
resourceRetriever,
supportsControls
);
return commandsSpecificSuggestions;
}
if (astContext.type === 'function') {
return getFunctionArgsSuggestions(
const functionsSpecificSuggestions = await getFunctionArgsSuggestions(
innerText,
ast,
astContext,
Expand All @@ -172,6 +189,7 @@ export async function suggest(
getVariables,
supportsControls
);
return functionsSpecificSuggestions;
}
if (astContext.type === 'list') {
return getListArgsSuggestions(
Expand All @@ -187,12 +205,17 @@ export async function suggest(
}

export function getFieldsByTypeRetriever(
queryString: string,
resourceRetriever?: ESQLCallbacks
queryForFields: string,
resourceRetriever?: ESQLCallbacks,
fullQuery?: string
): { getFieldsByType: GetColumnsByTypeFn; getFieldsMap: GetFieldsMapFn } {
const helpers = getFieldsByTypeHelper(queryString, resourceRetriever);
const helpers = getFieldsByTypeHelper(queryForFields, resourceRetriever);
const getVariables = resourceRetriever?.getVariables;
const supportsControls = resourceRetriever?.canSuggestVariables?.() ?? false;
const canSuggestVariables = resourceRetriever?.canSuggestVariables?.() ?? false;

const queryString = fullQuery ?? queryForFields;
const lastCharacterTyped = queryString[queryString.length - 1];
const lastCharIsQuestionMark = lastCharacterTyped === '?';
return {
getFieldsByType: async (
expectedType: Readonly<string> | Readonly<string[]> = 'any',
Expand All @@ -201,7 +224,7 @@ export function getFieldsByTypeRetriever(
) => {
const updatedOptions = {
...options,
supportsControls,
supportsControls: canSuggestVariables && !lastCharIsQuestionMark,
};
const fields = await helpers.getFieldsByType(expectedType, ignored);
return buildFieldsDefinitionsWithMetadata(fields, updatedOptions, getVariables);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export const ESQLLang: CustomLangModuleType<ESQLCallbacks> = {
},
getSuggestionProvider: (callbacks?: ESQLCallbacks): monaco.languages.CompletionItemProvider => {
return {
triggerCharacters: [',', '(', '=', ' ', '[', ''],
triggerCharacters: [',', '(', '=', ' ', '[', '', '?'],
async provideCompletionItems(
model: monaco.editor.ITextModel,
position: monaco.Position,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ async function getHoverItemForFunction(
buildQueryUntilPreviousCommand(ast, correctedQuery),
ast
);
const { getFieldsMap } = getFieldsByTypeRetriever(queryForFields, resourceRetriever);
const { getFieldsMap } = getFieldsByTypeRetriever(queryForFields, resourceRetriever, innerText);

const fnDefinition = getFunctionDefinition(node.name);
// early exit on no hit
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,70 @@ describe('helpers', () => {
);
expect(updatedQueryString).toBe('FROM my_index \n| STATS BY ?my_variable');
});

it('should adjust the query string for trailing question mark', () => {
const queryString = 'FROM my_index | STATS BY ?';
const cursorPosition = { column: 27, lineNumber: 1 } as monaco.Position;
const variable = '?my_variable';

const updatedQueryString = updateQueryStringWithVariable(
queryString,
variable,
cursorPosition
);
expect(updatedQueryString).toBe('FROM my_index | STATS BY ?my_variable');
});

it('should adjust the query string if there is a ? at the second last position', () => {
const queryString = 'FROM my_index | STATS PERCENTILE(bytes, ?)';
const cursorPosition = { column: 42, lineNumber: 1 } as monaco.Position;
const variable = '?my_variable';

const updatedQueryString = updateQueryStringWithVariable(
queryString,
variable,
cursorPosition
);
expect(updatedQueryString).toBe('FROM my_index | STATS PERCENTILE(bytes, ?my_variable)');
});

it('should adjust the query string if there is a ? at the last cursor position', () => {
const queryString =
'FROM my_index | STATS COUNT() BY BUCKET(@timestamp, ?, ?_tstart, ?_tend)';
const cursorPosition = {
lineNumber: 1,
column: 54,
} as monaco.Position;
const variable = '?my_variable';

const updatedQueryString = updateQueryStringWithVariable(
queryString,
variable,
cursorPosition
);
expect(updatedQueryString).toBe(
'FROM my_index | STATS COUNT() BY BUCKET(@timestamp, ?my_variable, ?_tstart, ?_tend)'
);
});

it('should adjust the query string if there is a ? at the last cursor position for multilines query', () => {
const queryString =
'FROM my_index \n| STATS COUNT() BY BUCKET(@timestamp, ?, ?_tstart, ?_tend)';
const cursorPosition = {
lineNumber: 2,
column: 40,
} as monaco.Position;
const variable = '?my_variable';

const updatedQueryString = updateQueryStringWithVariable(
queryString,
variable,
cursorPosition
);
expect(updatedQueryString).toBe(
'FROM my_index \n| STATS COUNT() BY BUCKET(@timestamp, ?my_variable, ?_tstart, ?_tend)'
);
});
});

describe('getQueryForFields', () => {
Expand Down
Loading