mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
parent
969a122eea
commit
928254db4b
9 changed files with 168 additions and 33 deletions
|
@ -21,7 +21,7 @@ const { accessSync, mkdirSync } = require('fs');
|
||||||
const { join } = require('path');
|
const { join } = require('path');
|
||||||
const simpleGit = require('simple-git');
|
const simpleGit = require('simple-git');
|
||||||
|
|
||||||
// Note: The generated whitelists have not yet been merged to master
|
// Note: The generated allowlists have not yet been merged to ES
|
||||||
// so this script may fail until code in this branch has been merged:
|
// so this script may fail until code in this branch has been merged:
|
||||||
// https://github.com/stu-elastic/elasticsearch/tree/scripting/whitelists
|
// https://github.com/stu-elastic/elasticsearch/tree/scripting/whitelists
|
||||||
const esRepo = 'https://github.com/elastic/elasticsearch.git';
|
const esRepo = 'https://github.com/elastic/elasticsearch.git';
|
||||||
|
|
|
@ -18,9 +18,9 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ID } from './constants';
|
import { ID } from './constants';
|
||||||
import { lexerRules } from './lexer_rules';
|
import { lexerRules, languageConfiguration } from './lexer_rules';
|
||||||
import { getSuggestionProvider } from './language';
|
import { getSuggestionProvider } from './language';
|
||||||
|
|
||||||
export const PainlessLang = { ID, getSuggestionProvider, lexerRules };
|
export const PainlessLang = { ID, getSuggestionProvider, lexerRules, languageConfiguration };
|
||||||
|
|
||||||
export { PainlessContext, PainlessAutocompleteField } from './types';
|
export { PainlessContext, PainlessAutocompleteField } from './types';
|
||||||
|
|
|
@ -17,4 +17,4 @@
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export { lexerRules } from './painless';
|
export { lexerRules, languageConfiguration } from './painless';
|
||||||
|
|
|
@ -180,3 +180,17 @@ export const lexerRules = {
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
} as Language;
|
} as Language;
|
||||||
|
|
||||||
|
export const languageConfiguration: monaco.languages.LanguageConfiguration = {
|
||||||
|
brackets: [
|
||||||
|
['{', '}'],
|
||||||
|
['[', ']'],
|
||||||
|
['(', ')'],
|
||||||
|
],
|
||||||
|
autoClosingPairs: [
|
||||||
|
{ open: '{', close: '}' },
|
||||||
|
{ open: '[', close: ']' },
|
||||||
|
{ open: '(', close: ')' },
|
||||||
|
{ open: '"', close: '"' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
|
@ -18,7 +18,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { PainlessCompletionItem } from '../../types';
|
import { PainlessCompletionItem } from '../../types';
|
||||||
import { lexerRules } from '../../lexer_rules';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getStaticSuggestions,
|
getStaticSuggestions,
|
||||||
|
@ -26,17 +25,11 @@ import {
|
||||||
getClassMemberSuggestions,
|
getClassMemberSuggestions,
|
||||||
getPrimitives,
|
getPrimitives,
|
||||||
getConstructorSuggestions,
|
getConstructorSuggestions,
|
||||||
|
getKeywords,
|
||||||
Suggestion,
|
Suggestion,
|
||||||
} from './autocomplete';
|
} from './autocomplete';
|
||||||
|
|
||||||
const keywords: PainlessCompletionItem[] = lexerRules.keywords.map((keyword) => {
|
const keywords: PainlessCompletionItem[] = getKeywords();
|
||||||
return {
|
|
||||||
label: keyword,
|
|
||||||
kind: 'keyword',
|
|
||||||
documentation: 'Keyword: char',
|
|
||||||
insertText: keyword,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const testSuggestions: Suggestion[] = [
|
const testSuggestions: Suggestion[] = [
|
||||||
{
|
{
|
||||||
|
@ -101,7 +94,7 @@ const testSuggestions: Suggestion[] = [
|
||||||
describe('Autocomplete lib', () => {
|
describe('Autocomplete lib', () => {
|
||||||
describe('Static suggestions', () => {
|
describe('Static suggestions', () => {
|
||||||
test('returns static suggestions', () => {
|
test('returns static suggestions', () => {
|
||||||
expect(getStaticSuggestions(testSuggestions, false)).toEqual({
|
expect(getStaticSuggestions({ suggestions: testSuggestions })).toEqual({
|
||||||
isIncomplete: false,
|
isIncomplete: false,
|
||||||
suggestions: [
|
suggestions: [
|
||||||
{
|
{
|
||||||
|
@ -134,12 +127,26 @@ describe('Autocomplete lib', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('returns doc keyword when fields exist', () => {
|
test('returns doc keyword when fields exist', () => {
|
||||||
const autocompletion = getStaticSuggestions(testSuggestions, true);
|
const autocompletion = getStaticSuggestions({
|
||||||
|
suggestions: testSuggestions,
|
||||||
|
hasFields: true,
|
||||||
|
});
|
||||||
const docSuggestion = autocompletion.suggestions.find(
|
const docSuggestion = autocompletion.suggestions.find(
|
||||||
(suggestion) => suggestion.label === 'doc'
|
(suggestion) => suggestion.label === 'doc'
|
||||||
);
|
);
|
||||||
expect(Boolean(docSuggestion)).toBe(true);
|
expect(Boolean(docSuggestion)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('returns emit keyword for runtime fields', () => {
|
||||||
|
const autocompletion = getStaticSuggestions({
|
||||||
|
suggestions: testSuggestions,
|
||||||
|
isRuntimeContext: true,
|
||||||
|
});
|
||||||
|
const emitSuggestion = autocompletion.suggestions.find(
|
||||||
|
(suggestion) => suggestion.label === 'emit'
|
||||||
|
);
|
||||||
|
expect(Boolean(emitSuggestion)).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getPrimitives()', () => {
|
describe('getPrimitives()', () => {
|
||||||
|
|
|
@ -53,14 +53,42 @@ export interface Suggestion extends PainlessCompletionItem {
|
||||||
constructorDefinition?: PainlessCompletionItem;
|
constructorDefinition?: PainlessCompletionItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
const keywords: PainlessCompletionItem[] = lexerRules.keywords.map((keyword) => {
|
export const getKeywords = (): PainlessCompletionItem[] => {
|
||||||
return {
|
const lexerKeywords: PainlessCompletionItem[] = lexerRules.keywords.map((keyword) => {
|
||||||
label: keyword,
|
return {
|
||||||
kind: 'keyword',
|
label: keyword,
|
||||||
documentation: 'Keyword: char',
|
kind: 'keyword',
|
||||||
insertText: keyword,
|
documentation: `Keyword: ${keyword}`,
|
||||||
};
|
insertText: keyword,
|
||||||
});
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const allKeywords: PainlessCompletionItem[] = [
|
||||||
|
...lexerKeywords,
|
||||||
|
{
|
||||||
|
label: 'params',
|
||||||
|
kind: 'keyword',
|
||||||
|
documentation: i18n.translate(
|
||||||
|
'monaco.painlessLanguage.autocomplete.paramsKeywordDescription',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Access variables passed into the script.',
|
||||||
|
}
|
||||||
|
),
|
||||||
|
insertText: 'params',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return allKeywords;
|
||||||
|
};
|
||||||
|
|
||||||
|
const runtimeContexts: PainlessContext[] = [
|
||||||
|
'boolean_script_field_script_field',
|
||||||
|
'date_script_field',
|
||||||
|
'double_script_field_script_field',
|
||||||
|
'ip_script_field_script_field',
|
||||||
|
'long_script_field_script_field',
|
||||||
|
'string_script_field_script_field',
|
||||||
|
];
|
||||||
|
|
||||||
const mapContextToData: { [key: string]: { suggestions: any[] } } = {
|
const mapContextToData: { [key: string]: { suggestions: any[] } } = {
|
||||||
painless_test: painlessTestContext,
|
painless_test: painlessTestContext,
|
||||||
|
@ -75,16 +103,23 @@ const mapContextToData: { [key: string]: { suggestions: any[] } } = {
|
||||||
string_script_field_script_field: stringScriptFieldScriptFieldContext,
|
string_script_field_script_field: stringScriptFieldScriptFieldContext,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getStaticSuggestions = (
|
export const getStaticSuggestions = ({
|
||||||
suggestions: Suggestion[],
|
suggestions,
|
||||||
hasFields: boolean
|
hasFields,
|
||||||
): PainlessCompletionResult => {
|
isRuntimeContext,
|
||||||
|
}: {
|
||||||
|
suggestions: Suggestion[];
|
||||||
|
hasFields?: boolean;
|
||||||
|
isRuntimeContext?: boolean;
|
||||||
|
}): PainlessCompletionResult => {
|
||||||
const classSuggestions: PainlessCompletionItem[] = suggestions.map((suggestion) => {
|
const classSuggestions: PainlessCompletionItem[] = suggestions.map((suggestion) => {
|
||||||
const { properties, constructorDefinition, ...rootSuggestion } = suggestion;
|
const { properties, constructorDefinition, ...rootSuggestion } = suggestion;
|
||||||
return rootSuggestion;
|
return rootSuggestion;
|
||||||
});
|
});
|
||||||
|
|
||||||
const keywordSuggestions: PainlessCompletionItem[] = hasFields
|
const keywords = getKeywords();
|
||||||
|
|
||||||
|
let keywordSuggestions: PainlessCompletionItem[] = hasFields
|
||||||
? [
|
? [
|
||||||
...keywords,
|
...keywords,
|
||||||
{
|
{
|
||||||
|
@ -102,6 +137,23 @@ export const getStaticSuggestions = (
|
||||||
]
|
]
|
||||||
: keywords;
|
: keywords;
|
||||||
|
|
||||||
|
keywordSuggestions = isRuntimeContext
|
||||||
|
? [
|
||||||
|
...keywordSuggestions,
|
||||||
|
{
|
||||||
|
label: 'emit',
|
||||||
|
kind: 'keyword',
|
||||||
|
documentation: i18n.translate(
|
||||||
|
'monaco.painlessLanguage.autocomplete.emitKeywordDescription',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Emit value without returning.',
|
||||||
|
}
|
||||||
|
),
|
||||||
|
insertText: 'emit',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: keywordSuggestions;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isIncomplete: false,
|
isIncomplete: false,
|
||||||
suggestions: [...classSuggestions, ...keywordSuggestions],
|
suggestions: [...classSuggestions, ...keywordSuggestions],
|
||||||
|
@ -176,6 +228,12 @@ export const getAutocompleteSuggestions = (
|
||||||
// What the user is currently typing
|
// What the user is currently typing
|
||||||
const activeTyping = words[words.length - 1];
|
const activeTyping = words[words.length - 1];
|
||||||
const primitives = getPrimitives(suggestions);
|
const primitives = getPrimitives(suggestions);
|
||||||
|
// This logic may end up needing to be more robust as we integrate autocomplete into more editors
|
||||||
|
// For now, we're assuming there is a list of painless contexts that are only applicable in runtime fields
|
||||||
|
const isRuntimeContext = runtimeContexts.includes(painlessContext);
|
||||||
|
// "text" field types are not available in doc values and should be removed for autocompletion
|
||||||
|
const filteredFields = fields?.filter((field) => field.type !== 'text');
|
||||||
|
const hasFields = Boolean(filteredFields?.length);
|
||||||
|
|
||||||
let autocompleteSuggestions: PainlessCompletionResult = {
|
let autocompleteSuggestions: PainlessCompletionResult = {
|
||||||
isIncomplete: false,
|
isIncomplete: false,
|
||||||
|
@ -184,13 +242,13 @@ export const getAutocompleteSuggestions = (
|
||||||
|
|
||||||
if (isConstructorInstance(words)) {
|
if (isConstructorInstance(words)) {
|
||||||
autocompleteSuggestions = getConstructorSuggestions(suggestions);
|
autocompleteSuggestions = getConstructorSuggestions(suggestions);
|
||||||
} else if (fields && isDeclaringField(activeTyping)) {
|
} else if (filteredFields && isDeclaringField(activeTyping)) {
|
||||||
autocompleteSuggestions = getFieldSuggestions(fields);
|
autocompleteSuggestions = getFieldSuggestions(filteredFields);
|
||||||
} else if (isAccessingProperty(activeTyping)) {
|
} else if (isAccessingProperty(activeTyping)) {
|
||||||
const className = activeTyping.substring(0, activeTyping.length - 1).split('.')[0];
|
const className = activeTyping.substring(0, activeTyping.length - 1).split('.')[0];
|
||||||
autocompleteSuggestions = getClassMemberSuggestions(suggestions, className);
|
autocompleteSuggestions = getClassMemberSuggestions(suggestions, className);
|
||||||
} else if (showStaticSuggestions(activeTyping, words, primitives)) {
|
} else if (showStaticSuggestions(activeTyping, words, primitives)) {
|
||||||
autocompleteSuggestions = getStaticSuggestions(suggestions, Boolean(fields?.length));
|
autocompleteSuggestions = getStaticSuggestions({ suggestions, hasFields, isRuntimeContext });
|
||||||
}
|
}
|
||||||
return autocompleteSuggestions;
|
return autocompleteSuggestions;
|
||||||
};
|
};
|
||||||
|
|
|
@ -23,6 +23,8 @@ import {
|
||||||
hasDeclaredType,
|
hasDeclaredType,
|
||||||
isAccessingProperty,
|
isAccessingProperty,
|
||||||
showStaticSuggestions,
|
showStaticSuggestions,
|
||||||
|
isDefiningString,
|
||||||
|
isDefiningBoolean,
|
||||||
} from './autocomplete_utils';
|
} from './autocomplete_utils';
|
||||||
|
|
||||||
const primitives = ['boolean', 'int', 'char', 'float', 'double'];
|
const primitives = ['boolean', 'int', 'char', 'float', 'double'];
|
||||||
|
@ -62,6 +64,24 @@ describe('Utils', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('isDefiningBoolean()', () => {
|
||||||
|
test('returns true or false depending if an array contains a boolean type and "=" token at a specific index', () => {
|
||||||
|
expect(isDefiningBoolean(['boolean', 'myBoolean', '=', 't'])).toEqual(true);
|
||||||
|
expect(isDefiningBoolean(['double', 'myBoolean', '=', 't'])).toEqual(false);
|
||||||
|
expect(isDefiningBoolean(['boolean', '='])).toEqual(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isDefiningString()', () => {
|
||||||
|
test('returns true or false depending if active typing contains a single or double quotation mark', () => {
|
||||||
|
expect(isDefiningString(`'mystring'`)).toEqual(true);
|
||||||
|
expect(isDefiningString(`"mystring"`)).toEqual(true);
|
||||||
|
expect(isDefiningString(`'`)).toEqual(true);
|
||||||
|
expect(isDefiningString(`"`)).toEqual(true);
|
||||||
|
expect(isDefiningString('mystring')).toEqual(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('showStaticSuggestions()', () => {
|
describe('showStaticSuggestions()', () => {
|
||||||
test('returns true or false depending if a type is declared or the string contains a "."', () => {
|
test('returns true or false depending if a type is declared or the string contains a "."', () => {
|
||||||
expect(showStaticSuggestions('a', ['a'], primitives)).toEqual(true);
|
expect(showStaticSuggestions('a', ['a'], primitives)).toEqual(true);
|
||||||
|
|
|
@ -36,11 +36,39 @@ export const isAccessingProperty = (activeTyping: string): boolean => {
|
||||||
/**
|
/**
|
||||||
* If the preceding word is a primitive type, e.g., "boolean",
|
* If the preceding word is a primitive type, e.g., "boolean",
|
||||||
* we assume the user is declaring a variable and will skip autocomplete
|
* we assume the user is declaring a variable and will skip autocomplete
|
||||||
|
*
|
||||||
|
* Note: this isn't entirely exhaustive. For example, "def myVar =" is not included in context
|
||||||
|
* It's also acceptable to use a class as a type, e.g., "String myVar ="
|
||||||
*/
|
*/
|
||||||
export const hasDeclaredType = (activeLineWords: string[], primitives: string[]): boolean => {
|
export const hasDeclaredType = (activeLineWords: string[], primitives: string[]): boolean => {
|
||||||
return activeLineWords.length === 2 && primitives.includes(activeLineWords[0]);
|
return activeLineWords.length === 2 && primitives.includes(activeLineWords[0]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the active line words contains the "boolean" type and "=" token,
|
||||||
|
* we assume the user is defining a boolean value and skip autocomplete
|
||||||
|
*/
|
||||||
|
export const isDefiningBoolean = (activeLineWords: string[]): boolean => {
|
||||||
|
if (activeLineWords.length === 4) {
|
||||||
|
const maybePrimitiveType = activeLineWords[0];
|
||||||
|
const maybeEqualToken = activeLineWords[2];
|
||||||
|
return maybePrimitiveType === 'boolean' && maybeEqualToken === '=';
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the active typing contains a start or end quotation mark,
|
||||||
|
* we assume the user is defining a string and skip autocomplete
|
||||||
|
*/
|
||||||
|
export const isDefiningString = (activeTyping: string): boolean => {
|
||||||
|
const quoteTokens = [`'`, `"`];
|
||||||
|
const activeTypingParts = activeTyping.split('');
|
||||||
|
const startCharacter = activeTypingParts[0];
|
||||||
|
const endCharacter = activeTypingParts[activeTypingParts.length - 1];
|
||||||
|
return quoteTokens.includes(startCharacter) || quoteTokens.includes(endCharacter);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the preceding word contains the "new" keyword
|
* Check if the preceding word contains the "new" keyword
|
||||||
*/
|
*/
|
||||||
|
@ -62,8 +90,10 @@ export const isDeclaringField = (activeTyping: string): boolean => {
|
||||||
/**
|
/**
|
||||||
* Static suggestions serve as a catch-all most of the time
|
* Static suggestions serve as a catch-all most of the time
|
||||||
* However, there are a few situations where we do not want to show them and instead default to the built-in monaco (abc) autocomplete
|
* However, there are a few situations where we do not want to show them and instead default to the built-in monaco (abc) autocomplete
|
||||||
* 1. If the preceding word is a type, e.g., "boolean", we assume the user is declaring a variable name
|
* 1. If the preceding word is a primitive type, e.g., "boolean", we assume the user is declaring a variable name
|
||||||
* 2. If the string contains a "dot" character, we assume the user is attempting to access a property that we do not have information for
|
* 2. If the string contains a "dot" character, we assume the user is attempting to access a property that we do not have information for
|
||||||
|
* 3. If the user is defining a variable with a boolean type, e.g., "boolean myBoolean ="
|
||||||
|
* 4. If the user is defining a string
|
||||||
*/
|
*/
|
||||||
export const showStaticSuggestions = (
|
export const showStaticSuggestions = (
|
||||||
activeTyping: string,
|
activeTyping: string,
|
||||||
|
@ -72,5 +102,10 @@ export const showStaticSuggestions = (
|
||||||
): boolean => {
|
): boolean => {
|
||||||
const activeTypingParts = activeTyping.split('.');
|
const activeTypingParts = activeTyping.split('.');
|
||||||
|
|
||||||
return hasDeclaredType(activeLineWords, primitives) === false && activeTypingParts.length === 1;
|
return (
|
||||||
|
hasDeclaredType(activeLineWords, primitives) === false &&
|
||||||
|
isDefiningBoolean(activeLineWords) === false &&
|
||||||
|
isDefiningString(activeTyping) === false &&
|
||||||
|
activeTypingParts.length === 1
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -36,6 +36,7 @@ monaco.languages.setMonarchTokensProvider(XJsonLang.ID, XJsonLang.lexerRules);
|
||||||
monaco.languages.setLanguageConfiguration(XJsonLang.ID, XJsonLang.languageConfiguration);
|
monaco.languages.setLanguageConfiguration(XJsonLang.ID, XJsonLang.languageConfiguration);
|
||||||
monaco.languages.register({ id: PainlessLang.ID });
|
monaco.languages.register({ id: PainlessLang.ID });
|
||||||
monaco.languages.setMonarchTokensProvider(PainlessLang.ID, PainlessLang.lexerRules);
|
monaco.languages.setMonarchTokensProvider(PainlessLang.ID, PainlessLang.lexerRules);
|
||||||
|
monaco.languages.setLanguageConfiguration(PainlessLang.ID, PainlessLang.languageConfiguration);
|
||||||
monaco.languages.register({ id: EsqlLang.ID });
|
monaco.languages.register({ id: EsqlLang.ID });
|
||||||
monaco.languages.setMonarchTokensProvider(EsqlLang.ID, EsqlLang.lexerRules);
|
monaco.languages.setMonarchTokensProvider(EsqlLang.ID, EsqlLang.lexerRules);
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue