[ES|QL] Add hints upon hover for function argument types and time system types (#191881)

## Summary

This PR adds hints list of acceptable functional argument types when
user hovers over text or the empty space before cursor.


https://github.com/user-attachments/assets/280598bb-f848-4cb8-b1e5-afd6509bab56

In addition, it also adds some helpful hints to guide towards named
system params:
- If argument can be a constant date, it will suggest ?t_start and
?t_end
- If user hovers over ?t_start and ?t_end params, and the cursor is
positioned to be at least 3 characters long (e.g. if it can guess
whether the text is start or end) then it will show an explanation what
the params mean. This is helpful for some pre-populated queries or
snippet.

<img width="1144" alt="image"
src="https://github.com/user-attachments/assets/b39a2c44-abc8-4008-978c-41fc8fd4cc34">




### Checklist

Delete any items that are not applicable to this PR.

- [ ] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [ ] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
- [ ] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [ ] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [ ] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [ ] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [ ] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)


### Risk Matrix

Delete this section if it is not applicable to this PR.

Before closing this PR, invite QA, stakeholders, and other developers to
identify risks that should be tested prior to the change/feature
release.

When forming the risk matrix, consider some of the following examples
and how they may potentially impact the change:

| Risk | Probability | Severity | Mitigation/Notes |

|---------------------------|-------------|----------|-------------------------|
| Multiple Spaces&mdash;unexpected behavior in non-default Kibana Space.
| Low | High | Integration tests will verify that all features are still
supported in non-default Kibana Space and when user switches between
spaces. |
| Multiple nodes&mdash;Elasticsearch polling might have race conditions
when multiple Kibana nodes are polling for the same tasks. | High | Low
| Tasks are idempotent, so executing them multiple times will not result
in logical error, but will degrade performance. To test for this case we
add plenty of unit tests around this logic and document manual testing
procedure. |
| Code should gracefully handle cases when feature X or plugin Y are
disabled. | Medium | High | Unit tests will verify that any feature flag
or plugin combination still results in our service operational. |
| [See more potential risk
examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) |


### For maintainers

- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

---------

Co-authored-by: Stratoula Kalafateli <efstratia.kalafateli@elastic.co>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Quynh Nguyen (Quinn) 2024-09-11 15:11:37 -05:00 committed by GitHub
parent 6d0970bc75
commit 9b61a1ecd0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 339 additions and 142 deletions

View file

@ -114,6 +114,17 @@ export interface ESQLFunction<
args: ESQLAstItem[];
}
const isESQLAstBaseItem = (node: unknown): node is ESQLAstBaseItem =>
typeof node === 'object' &&
node !== null &&
Object.hasOwn(node, 'name') &&
Object.hasOwn(node, 'text');
export const isESQLFunction = (node: unknown): node is ESQLFunction =>
isESQLAstBaseItem(node) &&
Object.hasOwn(node, 'type') &&
(node as ESQLFunction).type === 'function';
export interface ESQLFunctionCallExpression extends ESQLFunction<'variadic-call'> {
subtype: 'variadic-call';
args: ESQLAstItem[];
@ -293,6 +304,10 @@ export interface ESQLNamedParamLiteral extends ESQLParamLiteral<'named'> {
value: string;
}
export const isESQLNamedParamLiteral = (node: ESQLAstItem): node is ESQLNamedParamLiteral =>
isESQLAstBaseItem(node) &&
(node as ESQLNamedParamLiteral).literalType === 'param' &&
(node as ESQLNamedParamLiteral).paramType === 'named';
/**
* *Positional* parameter is a question mark followed by a number "?1".
*

View file

@ -30,11 +30,9 @@ import {
isAssignment,
isAssignmentComplete,
isColumnItem,
isComma,
isFunctionItem,
isIncompleteItem,
isLiteralItem,
isMathFunction,
isOptionItem,
isRestartingExpression,
isSourceCommand,
@ -47,6 +45,7 @@ import {
getColumnExists,
findPreviousWord,
noCaseCompare,
correctQuerySyntax,
getColumnByName,
} from '../shared/helpers';
import { collectVariables, excludeVariablesFromCurrentCommand } from '../shared/variables';
@ -98,11 +97,8 @@ import {
getSourcesFromCommands,
getSupportedTypesForBinaryOperators,
isAggFunctionUsedAlready,
getCompatibleTypesToSuggestNext,
removeQuoteForSuggestedSources,
getValidFunctionSignaturesForPreviousArgs,
strictlyGetParamAtPosition,
isLiteralDateItem,
getValidSignaturesAndTypesToSuggestNext,
} from './helper';
import {
FunctionParameter,
@ -158,76 +154,6 @@ function getFinalSuggestions({ comma }: { comma?: boolean } = { comma: true }) {
return finalSuggestions;
}
/**
* This function count the number of unclosed brackets in order to
* locally fix the queryString to generate a valid AST
* A known limitation of this is that is not aware of commas "," or pipes "|"
* so it is not yet helpful on a multiple commands errors (a workaround it to pass each command here...)
* @param bracketType
* @param text
* @returns
*/
function countBracketsUnclosed(bracketType: '(' | '[' | '"' | '"""', text: string) {
const stack = [];
const closingBrackets = { '(': ')', '[': ']', '"': '"', '"""': '"""' };
for (let i = 0; i < text.length; i++) {
const substr = text.substring(i, i + bracketType.length);
if (substr === closingBrackets[bracketType] && stack.length) {
stack.pop();
} else if (substr === bracketType) {
stack.push(bracketType);
}
}
return stack.length;
}
/**
* This function attempts to correct the syntax of a partial query to make it valid.
*
* This is important because a syntactically-invalid query will not generate a good AST.
*
* @param _query
* @param context
* @returns
*/
function correctQuerySyntax(_query: string, context: EditorContext) {
let query = _query;
// check if all brackets are closed, otherwise close them
const unclosedRoundBrackets = countBracketsUnclosed('(', query);
const unclosedSquaredBrackets = countBracketsUnclosed('[', query);
const unclosedQuotes = countBracketsUnclosed('"', query);
const unclosedTripleQuotes = countBracketsUnclosed('"""', query);
// if it's a comma by the user or a forced trigger by a function argument suggestion
// add a marker to make the expression still valid
const charThatNeedMarkers = [',', ':'];
if (
(context.triggerCharacter && charThatNeedMarkers.includes(context.triggerCharacter)) ||
// monaco.editor.CompletionTriggerKind['Invoke'] === 0
(context.triggerKind === 0 && unclosedRoundBrackets === 0) ||
(context.triggerCharacter === ' ' && isMathFunction(query, query.length)) ||
isComma(query.trimEnd()[query.trimEnd().length - 1])
) {
query += EDITOR_MARKER;
}
// if there are unclosed brackets, close them
if (unclosedRoundBrackets || unclosedSquaredBrackets || unclosedQuotes) {
for (const [char, count] of [
['"""', unclosedTripleQuotes],
['"', unclosedQuotes],
[')', unclosedRoundBrackets],
[']', unclosedSquaredBrackets],
]) {
if (count) {
// inject the closing brackets
query += Array(count).fill(char).join('');
}
}
}
return query;
}
export async function suggest(
fullText: string,
offset: number,
@ -332,7 +258,7 @@ export async function suggest(
return [];
}
function getFieldsByTypeRetriever(
export function getFieldsByTypeRetriever(
queryString: string,
resourceRetriever?: ESQLCallbacks
): { getFieldsByType: GetFieldsByTypeFn; getFieldsMap: GetFieldsMapFn } {
@ -1290,18 +1216,6 @@ async function getFunctionArgsSuggestions(
fields: fieldsMap,
variables: anyVariables,
};
const enrichedArgs = node.args.map((nodeArg) => {
let dataType = extractTypeFromASTArg(nodeArg, references);
// For named system time parameters ?start and ?end, make sure it's compatiable
if (isLiteralDateItem(nodeArg)) {
dataType = 'date';
}
return { ...nodeArg, dataType } as ESQLAstItem & { dataType: string };
});
const variablesExcludingCurrentCommandOnes = excludeVariablesFromCurrentCommand(
commands,
command,
@ -1309,30 +1223,10 @@ async function getFunctionArgsSuggestions(
innerText
);
// pick the type of the next arg
const shouldGetNextArgument = node.text.includes(EDITOR_MARKER);
let argIndex = Math.max(node.args.length, 0);
if (!shouldGetNextArgument && argIndex) {
argIndex -= 1;
}
const { typesToSuggestNext, hasMoreMandatoryArgs, enrichedArgs, argIndex } =
getValidSignaturesAndTypesToSuggestNext(node, references, fnDefinition, fullText, offset);
const arg: ESQLAstItem = enrichedArgs[argIndex];
const validSignatures = getValidFunctionSignaturesForPreviousArgs(
fnDefinition,
enrichedArgs,
argIndex
);
// Retrieve unique of types that are compatiable for the current arg
const typesToSuggestNext = getCompatibleTypesToSuggestNext(fnDefinition, enrichedArgs, argIndex);
const hasMoreMandatoryArgs = !validSignatures
// Types available to suggest next after this argument is completed
.map((signature) => strictlyGetParamAtPosition(signature, argIndex + 1))
// when a param is null, it means param is optional
// If there's at least one param that is optional, then
// no need to suggest comma
.some((p) => p === null || p?.optional === true);
// Whether to prepend comma to suggestion string
// E.g. if true, "fieldName" -> "fieldName, "
const alreadyHasComma = fullText ? fullText[offset] === ',' : false;

View file

@ -438,6 +438,20 @@ export function getCompatibleLiterals(
return suggestions;
}
export const TIME_SYSTEM_DESCRIPTIONS = {
'?t_start': i18n.translate(
'kbn-esql-validation-autocomplete.esql.autocomplete.timeSystemParamStart',
{
defaultMessage: 'The start time from the date picker',
}
),
'?t_end': i18n.translate(
'kbn-esql-validation-autocomplete.esql.autocomplete.timeSystemParamEnd',
{
defaultMessage: 'The end time from the date picker',
}
),
};
export function getDateLiterals(options?: {
advanceCursorAndOpenSuggestions?: boolean;
addComma?: boolean;

View file

@ -19,6 +19,9 @@ import {
import type { SuggestionRawDefinition } from './types';
import { compareTypesWithLiterals } from '../shared/esql_types';
import { TIME_SYSTEM_PARAMS } from './factories';
import { EDITOR_MARKER } from '../shared/constants';
import { extractTypeFromASTArg } from './autocomplete';
import { ESQLRealField, ESQLVariable } from '../validation/types';
function extractFunctionArgs(args: ESQLAstItem[]): ESQLFunction[] {
return args.flatMap((arg) => (isAssignment(arg) ? arg.args[1] : arg)).filter(isFunctionItem);
@ -212,7 +215,8 @@ export function getOverlapRange(
};
}
function isValidDateString(dateString: string): boolean {
function isValidDateString(dateString: unknown): boolean {
if (typeof dateString !== 'string') return false;
const timestamp = Date.parse(dateString.replace(/\"/g, ''));
return !isNaN(timestamp);
}
@ -229,6 +233,63 @@ export function isLiteralDateItem(nodeArg: ESQLAstItem): boolean {
// If text is ?start or ?end, it's a system time parameter
(TIME_SYSTEM_PARAMS.includes(nodeArg.text) ||
// Or if it's a string generated by date picker
isValidDateString(nodeArg.text))
isValidDateString(nodeArg.value))
);
}
export function getValidSignaturesAndTypesToSuggestNext(
node: ESQLFunction,
references: { fields: Map<string, ESQLRealField>; variables: Map<string, ESQLVariable[]> },
fnDefinition: FunctionDefinition,
fullText: string,
offset: number
) {
const enrichedArgs = node.args.map((nodeArg) => {
let dataType = extractTypeFromASTArg(nodeArg, references);
// For named system time parameters ?start and ?end, make sure it's compatiable
if (isLiteralDateItem(nodeArg)) {
dataType = 'date';
}
return { ...nodeArg, dataType } as ESQLAstItem & { dataType: string };
});
// pick the type of the next arg
const shouldGetNextArgument = node.text.includes(EDITOR_MARKER);
let argIndex = Math.max(node.args.length, 0);
if (!shouldGetNextArgument && argIndex) {
argIndex -= 1;
}
const validSignatures = getValidFunctionSignaturesForPreviousArgs(
fnDefinition,
enrichedArgs,
argIndex
);
// Retrieve unique of types that are compatiable for the current arg
const typesToSuggestNext = getCompatibleTypesToSuggestNext(fnDefinition, enrichedArgs, argIndex);
const hasMoreMandatoryArgs = !validSignatures
// Types available to suggest next after this argument is completed
.map((signature) => strictlyGetParamAtPosition(signature, argIndex + 1))
// when a param is null, it means param is optional
// If there's at least one param that is optional, then
// no need to suggest comma
.some((p) => p === null || p?.optional === true);
// Whether to prepend comma to suggestion string
// E.g. if true, "fieldName" -> "fieldName, "
const alreadyHasComma = fullText ? fullText[offset] === ',' : false;
const shouldAddComma =
hasMoreMandatoryArgs && fnDefinition.type !== 'builtin' && !alreadyHasComma;
const currentArg = enrichedArgs[argIndex];
return {
shouldAddComma,
typesToSuggestNext,
validSignatures,
hasMoreMandatoryArgs,
enrichedArgs,
argIndex,
currentArg,
};
}

View file

@ -79,8 +79,10 @@ export const compareTypesWithLiterals = (
// date_period is day/week/month/year interval
// time_literal includes time_duration and date_period
// So they are equivalent AST's 'timeInterval' (a date unit constant: e.g. 1 year, 15 month)
if (a === 'time_literal' || a === 'time_duration') return b === 'timeInterval';
if (b === 'time_literal' || b === 'time_duration') return a === 'timeInterval';
if (a === 'time_literal' || a === 'time_duration' || a === 'date_period')
return b === 'timeInterval';
if (b === 'time_literal' || b === 'time_duration' || b === 'date_period')
return a === 'timeInterval';
if (a === 'time_literal') return b === 'time_duration';
if (b === 'time_literal') return a === 'time_duration';

View file

@ -48,6 +48,8 @@ import type { ESQLRealField, ESQLVariable, ReferenceMaps } from '../validation/t
import { removeMarkerArgFromArgsList } from './context';
import { isNumericDecimalType } from './esql_types';
import type { ReasonTypes } from './types';
import { EDITOR_MARKER } from './constants';
import type { EditorContext } from '../autocomplete/types';
export function nonNullable<T>(v: T): v is NonNullable<T> {
return v != null;
@ -662,3 +664,73 @@ export const isParam = (x: unknown): x is ESQLParamLiteral =>
* Compares two strings in a case-insensitive manner
*/
export const noCaseCompare = (a: string, b: string) => a.toLowerCase() === b.toLowerCase();
/**
* This function count the number of unclosed brackets in order to
* locally fix the queryString to generate a valid AST
* A known limitation of this is that is not aware of commas "," or pipes "|"
* so it is not yet helpful on a multiple commands errors (a workaround it to pass each command here...)
* @param bracketType
* @param text
* @returns
*/
export function countBracketsUnclosed(bracketType: '(' | '[' | '"' | '"""', text: string) {
const stack = [];
const closingBrackets = { '(': ')', '[': ']', '"': '"', '"""': '"""' };
for (let i = 0; i < text.length; i++) {
const substr = text.substring(i, i + bracketType.length);
if (substr === closingBrackets[bracketType] && stack.length) {
stack.pop();
} else if (substr === bracketType) {
stack.push(bracketType);
}
}
return stack.length;
}
/**
* This function attempts to correct the syntax of a partial query to make it valid.
*
* This is important because a syntactically-invalid query will not generate a good AST.
*
* @param _query
* @param context
* @returns
*/
export function correctQuerySyntax(_query: string, context: EditorContext) {
let query = _query;
// check if all brackets are closed, otherwise close them
const unclosedRoundBrackets = countBracketsUnclosed('(', query);
const unclosedSquaredBrackets = countBracketsUnclosed('[', query);
const unclosedQuotes = countBracketsUnclosed('"', query);
const unclosedTripleQuotes = countBracketsUnclosed('"""', query);
// if it's a comma by the user or a forced trigger by a function argument suggestion
// add a marker to make the expression still valid
const charThatNeedMarkers = [',', ':'];
if (
(context.triggerCharacter && charThatNeedMarkers.includes(context.triggerCharacter)) ||
// monaco.editor.CompletionTriggerKind['Invoke'] === 0
(context.triggerKind === 0 && unclosedRoundBrackets === 0) ||
(context.triggerCharacter === ' ' && isMathFunction(query, query.length)) ||
isComma(query.trimEnd()[query.trimEnd().length - 1])
) {
query += EDITOR_MARKER;
}
// if there are unclosed brackets, close them
if (unclosedRoundBrackets || unclosedSquaredBrackets || unclosedQuotes) {
for (const [char, count] of [
['"""', unclosedTripleQuotes],
['"', unclosedQuotes],
[')', unclosedRoundBrackets],
[']', unclosedSquaredBrackets],
]) {
if (count) {
// inject the closing brackets
query += Array(count).fill(char).join('');
}
}
}
return query;
}

View file

@ -84,7 +84,11 @@ function createCustomCallbackMocks(
function createModelAndPosition(text: string, string: string) {
return {
model: { getValue: () => text } as monaco.editor.ITextModel,
model: {
getValue: () => text,
getLineCount: () => text.split('\n').length,
getLineMaxColumn: (lineNumber: number) => text.split('\n')[lineNumber - 1].length,
} as unknown as monaco.editor.ITextModel,
// bumo the column by one as the internal logic has a -1 offset when converting frmo monaco
position: { lineNumber: 1, column: text.lastIndexOf(string) + 1 } as monaco.Position,
};
@ -206,13 +210,17 @@ describe('hover', () => {
'nonExistentFn',
createFunctionContent
);
testSuggestions(`from a | stats avg(round(numberField))`, 'round', createFunctionContent);
testSuggestions(`from a | stats avg(round(numberField))`, 'round', () => {
return [
'**Acceptable types**: **double** | **integer** | **long**',
...createFunctionContent('round'),
];
});
testSuggestions(`from a | stats avg(round(numberField))`, 'avg', createFunctionContent);
testSuggestions(
`from a | stats avg(nonExistentFn(numberField))`,
'nonExistentFn',
createFunctionContent
);
testSuggestions(`from a | stats avg(nonExistentFn(numberField))`, 'nonExistentFn', () => [
'**Acceptable types**: **double** | **integer** | **long**',
...createFunctionContent('nonExistentFn'),
]);
testSuggestions(`from a | where round(numberField) > 0`, 'round', createFunctionContent);
});
});

View file

@ -8,7 +8,7 @@
*/
import { i18n } from '@kbn/i18n';
import type { AstProviderFn } from '@kbn/esql-ast';
import type { AstProviderFn, ESQLAstItem } from '@kbn/esql-ast';
import {
getAstContext,
getFunctionDefinition,
@ -18,9 +18,123 @@ import {
getCommandDefinition,
type ESQLCallbacks,
getPolicyHelper,
collectVariables,
ESQLRealField,
} from '@kbn/esql-validation-autocomplete';
import type { monaco } from '../../../monaco_imports';
import { correctQuerySyntax } from '@kbn/esql-validation-autocomplete/src/shared/helpers';
import type { EditorContext } from '@kbn/esql-validation-autocomplete/src/autocomplete/types';
import {
getQueryForFields,
getValidSignaturesAndTypesToSuggestNext,
} from '@kbn/esql-validation-autocomplete/src/autocomplete/helper';
import { buildQueryUntilPreviousCommand } from '@kbn/esql-validation-autocomplete/src/shared/resources_helpers';
import { getFieldsByTypeRetriever } from '@kbn/esql-validation-autocomplete/src/autocomplete/autocomplete';
import {
TIME_SYSTEM_DESCRIPTIONS,
TIME_SYSTEM_PARAMS,
} from '@kbn/esql-validation-autocomplete/src/autocomplete/factories';
import { isESQLFunction, isESQLNamedParamLiteral } from '@kbn/esql-ast/src/types';
import { monacoPositionToOffset } from '../shared/utils';
import { monaco } from '../../../monaco_imports';
const ACCEPTABLE_TYPES_HOVER = i18n.translate('monaco.esql.hover.acceptableTypes', {
defaultMessage: 'Acceptable types',
});
async function getHoverItemForFunction(
model: monaco.editor.ITextModel,
position: monaco.Position,
token: monaco.CancellationToken,
astProvider: AstProviderFn,
resourceRetriever?: ESQLCallbacks
) {
const context: EditorContext = {
triggerCharacter: ' ',
triggerKind: 1,
};
const fullText = model.getValue();
const offset = monacoPositionToOffset(fullText, position);
const innerText = fullText.substring(0, offset);
const correctedQuery = correctQuerySyntax(innerText, context);
const { ast } = await astProvider(correctedQuery);
const astContext = getAstContext(innerText, ast, offset);
const { node } = astContext;
const commands = ast;
if (isESQLFunction(node) && astContext.type === 'function') {
const queryForFields = getQueryForFields(
buildQueryUntilPreviousCommand(ast, correctedQuery),
ast
);
const { getFieldsMap } = getFieldsByTypeRetriever(queryForFields, resourceRetriever);
const fnDefinition = getFunctionDefinition(node.name);
// early exit on no hit
if (!fnDefinition) {
return undefined;
}
const fieldsMap: Map<string, ESQLRealField> = await getFieldsMap();
const anyVariables = collectVariables(commands, fieldsMap, innerText);
const references = {
fields: fieldsMap,
variables: anyVariables,
};
const { typesToSuggestNext, enrichedArgs } = getValidSignaturesAndTypesToSuggestNext(
node,
references,
fnDefinition,
fullText,
offset
);
const hoveredArg: ESQLAstItem & {
dataType: string;
} = enrichedArgs[enrichedArgs.length - 1];
const contents = [];
if (hoveredArg && isESQLNamedParamLiteral(hoveredArg)) {
const bestMatch = TIME_SYSTEM_PARAMS.find((p) => p.startsWith(hoveredArg.text));
// We only know if it's start or end after first 3 characters (?t_s or ?t_e)
if (hoveredArg.text.length > 3 && bestMatch) {
Object.entries(TIME_SYSTEM_DESCRIPTIONS).forEach(([key, value]) => {
contents.push({
value: `**${key}**: ${value}`,
});
});
}
}
if (typesToSuggestNext.length > 0) {
contents.push({
value: `**${ACCEPTABLE_TYPES_HOVER}**: ${typesToSuggestNext
.map(
({ type, constantOnly }) =>
`${constantOnly ? '_constant_ ' : ''}**${type}**` +
// If function arg is a constant date, helpfully suggest named time system params
(constantOnly && type === 'date' ? ` | ${TIME_SYSTEM_PARAMS.join(' | ')}` : '')
)
.join(' | ')}`,
});
}
const hints =
contents.length > 0
? {
range: new monaco.Range(
1,
1,
model.getLineCount(),
model.getLineMaxColumn(model.getLineCount())
),
contents,
}
: undefined;
return hints;
}
}
export async function getHoverItem(
model: monaco.editor.ITextModel,
@ -29,13 +143,28 @@ export async function getHoverItem(
astProvider: AstProviderFn,
resourceRetriever?: ESQLCallbacks
) {
const innerText = model.getValue();
const offset = monacoPositionToOffset(innerText, position);
const fullText = model.getValue();
const offset = monacoPositionToOffset(fullText, position);
const { ast } = await astProvider(fullText);
const astContext = getAstContext(fullText, ast, offset);
const { ast } = await astProvider(innerText);
const astContext = getAstContext(innerText, ast, offset);
const { getPolicyMetadata } = getPolicyHelper(resourceRetriever);
let hoverContent: monaco.languages.Hover = {
contents: [],
};
const hoverItemsForFunction = await getHoverItemForFunction(
model,
position,
token,
astProvider,
resourceRetriever
);
if (hoverItemsForFunction) {
hoverContent = hoverItemsForFunction;
}
if (['newCommand', 'list'].includes(astContext.type)) {
return { contents: [] };
}
@ -44,12 +173,12 @@ export async function getHoverItem(
const fnDefinition = getFunctionDefinition(astContext.node.name);
if (fnDefinition) {
return {
contents: [
hoverContent.contents.push(
...[
{ value: getFunctionSignatures(fnDefinition)[0].declaration },
{ value: fnDefinition.description },
],
};
]
);
}
}
@ -58,8 +187,8 @@ export async function getHoverItem(
if (isSourceItem(astContext.node) && astContext.node.sourceType === 'policy') {
const policyMetadata = await getPolicyMetadata(astContext.node.name);
if (policyMetadata) {
return {
contents: [
hoverContent.contents.push(
...[
{
value: `${i18n.translate('monaco.esql.hover.policyIndexes', {
defaultMessage: '**Indexes**',
@ -75,8 +204,8 @@ export async function getHoverItem(
defaultMessage: '**Fields**',
})}: ${policyMetadata.enrichFields.join(', ')}`,
},
],
};
]
);
}
}
if (isSettingItem(astContext.node)) {
@ -86,18 +215,17 @@ export async function getHoverItem(
);
if (settingDef) {
const mode = settingDef.values.find(({ name }) => name === astContext.node!.name)!;
return {
contents: [
hoverContent.contents.push(
...[
{ value: settingDef.description },
{
value: `**${mode.name}**: ${mode.description}`,
},
],
};
]
);
}
}
}
}
return { contents: [] };
return hoverContent;
}

View file

@ -555,6 +555,9 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
onLayoutChangeRef.current = onLayoutChange;
const codeEditorOptions: CodeEditorProps['options'] = {
hover: {
above: false,
},
accessibilitySupport: 'off',
autoIndent: 'none',
automaticLayout: true,