diff --git a/packages/kbn-monaco/scripts/utils/clone_es.js b/packages/kbn-monaco/scripts/utils/clone_es.js index 511cfd89fbf54..51063b8901731 100644 --- a/packages/kbn-monaco/scripts/utils/clone_es.js +++ b/packages/kbn-monaco/scripts/utils/clone_es.js @@ -21,7 +21,7 @@ const { accessSync, mkdirSync } = require('fs'); const { join } = require('path'); 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: // https://github.com/stu-elastic/elasticsearch/tree/scripting/whitelists const esRepo = 'https://github.com/elastic/elasticsearch.git'; diff --git a/packages/kbn-monaco/src/painless/index.ts b/packages/kbn-monaco/src/painless/index.ts index 4693fa2418b66..10c82d2ae6695 100644 --- a/packages/kbn-monaco/src/painless/index.ts +++ b/packages/kbn-monaco/src/painless/index.ts @@ -18,9 +18,9 @@ */ import { ID } from './constants'; -import { lexerRules } from './lexer_rules'; +import { lexerRules, languageConfiguration } from './lexer_rules'; import { getSuggestionProvider } from './language'; -export const PainlessLang = { ID, getSuggestionProvider, lexerRules }; +export const PainlessLang = { ID, getSuggestionProvider, lexerRules, languageConfiguration }; export { PainlessContext, PainlessAutocompleteField } from './types'; diff --git a/packages/kbn-monaco/src/painless/lexer_rules/index.ts b/packages/kbn-monaco/src/painless/lexer_rules/index.ts index 7cf9064c6aa51..718231b4fe0cd 100644 --- a/packages/kbn-monaco/src/painless/lexer_rules/index.ts +++ b/packages/kbn-monaco/src/painless/lexer_rules/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { lexerRules } from './painless'; +export { lexerRules, languageConfiguration } from './painless'; diff --git a/packages/kbn-monaco/src/painless/lexer_rules/painless.ts b/packages/kbn-monaco/src/painless/lexer_rules/painless.ts index 2f4383911c9ad..580c6f9499569 100644 --- a/packages/kbn-monaco/src/painless/lexer_rules/painless.ts +++ b/packages/kbn-monaco/src/painless/lexer_rules/painless.ts @@ -180,3 +180,17 @@ export const lexerRules = { ], }, } as Language; + +export const languageConfiguration: monaco.languages.LanguageConfiguration = { + brackets: [ + ['{', '}'], + ['[', ']'], + ['(', ')'], + ], + autoClosingPairs: [ + { open: '{', close: '}' }, + { open: '[', close: ']' }, + { open: '(', close: ')' }, + { open: '"', close: '"' }, + ], +}; diff --git a/packages/kbn-monaco/src/painless/worker/lib/autocomplete.test.ts b/packages/kbn-monaco/src/painless/worker/lib/autocomplete.test.ts index 8cc5d21d9d7e0..4a975596affba 100644 --- a/packages/kbn-monaco/src/painless/worker/lib/autocomplete.test.ts +++ b/packages/kbn-monaco/src/painless/worker/lib/autocomplete.test.ts @@ -18,7 +18,6 @@ */ import { PainlessCompletionItem } from '../../types'; -import { lexerRules } from '../../lexer_rules'; import { getStaticSuggestions, @@ -26,17 +25,11 @@ import { getClassMemberSuggestions, getPrimitives, getConstructorSuggestions, + getKeywords, Suggestion, } from './autocomplete'; -const keywords: PainlessCompletionItem[] = lexerRules.keywords.map((keyword) => { - return { - label: keyword, - kind: 'keyword', - documentation: 'Keyword: char', - insertText: keyword, - }; -}); +const keywords: PainlessCompletionItem[] = getKeywords(); const testSuggestions: Suggestion[] = [ { @@ -101,7 +94,7 @@ const testSuggestions: Suggestion[] = [ describe('Autocomplete lib', () => { describe('Static suggestions', () => { test('returns static suggestions', () => { - expect(getStaticSuggestions(testSuggestions, false)).toEqual({ + expect(getStaticSuggestions({ suggestions: testSuggestions })).toEqual({ isIncomplete: false, suggestions: [ { @@ -134,12 +127,26 @@ describe('Autocomplete lib', () => { }); test('returns doc keyword when fields exist', () => { - const autocompletion = getStaticSuggestions(testSuggestions, true); + const autocompletion = getStaticSuggestions({ + suggestions: testSuggestions, + hasFields: true, + }); const docSuggestion = autocompletion.suggestions.find( (suggestion) => suggestion.label === 'doc' ); 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()', () => { diff --git a/packages/kbn-monaco/src/painless/worker/lib/autocomplete.ts b/packages/kbn-monaco/src/painless/worker/lib/autocomplete.ts index e8e795e99b259..9bdaa298fb1c9 100644 --- a/packages/kbn-monaco/src/painless/worker/lib/autocomplete.ts +++ b/packages/kbn-monaco/src/painless/worker/lib/autocomplete.ts @@ -53,14 +53,42 @@ export interface Suggestion extends PainlessCompletionItem { constructorDefinition?: PainlessCompletionItem; } -const keywords: PainlessCompletionItem[] = lexerRules.keywords.map((keyword) => { - return { - label: keyword, - kind: 'keyword', - documentation: 'Keyword: char', - insertText: keyword, - }; -}); +export const getKeywords = (): PainlessCompletionItem[] => { + const lexerKeywords: PainlessCompletionItem[] = lexerRules.keywords.map((keyword) => { + return { + label: keyword, + kind: '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[] } } = { painless_test: painlessTestContext, @@ -75,16 +103,23 @@ const mapContextToData: { [key: string]: { suggestions: any[] } } = { string_script_field_script_field: stringScriptFieldScriptFieldContext, }; -export const getStaticSuggestions = ( - suggestions: Suggestion[], - hasFields: boolean -): PainlessCompletionResult => { +export const getStaticSuggestions = ({ + suggestions, + hasFields, + isRuntimeContext, +}: { + suggestions: Suggestion[]; + hasFields?: boolean; + isRuntimeContext?: boolean; +}): PainlessCompletionResult => { const classSuggestions: PainlessCompletionItem[] = suggestions.map((suggestion) => { const { properties, constructorDefinition, ...rootSuggestion } = suggestion; return rootSuggestion; }); - const keywordSuggestions: PainlessCompletionItem[] = hasFields + const keywords = getKeywords(); + + let keywordSuggestions: PainlessCompletionItem[] = hasFields ? [ ...keywords, { @@ -102,6 +137,23 @@ export const getStaticSuggestions = ( ] : keywords; + keywordSuggestions = isRuntimeContext + ? [ + ...keywordSuggestions, + { + label: 'emit', + kind: 'keyword', + documentation: i18n.translate( + 'monaco.painlessLanguage.autocomplete.emitKeywordDescription', + { + defaultMessage: 'Emit value without returning.', + } + ), + insertText: 'emit', + }, + ] + : keywordSuggestions; + return { isIncomplete: false, suggestions: [...classSuggestions, ...keywordSuggestions], @@ -176,6 +228,12 @@ export const getAutocompleteSuggestions = ( // What the user is currently typing const activeTyping = words[words.length - 1]; 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 = { isIncomplete: false, @@ -184,13 +242,13 @@ export const getAutocompleteSuggestions = ( if (isConstructorInstance(words)) { autocompleteSuggestions = getConstructorSuggestions(suggestions); - } else if (fields && isDeclaringField(activeTyping)) { - autocompleteSuggestions = getFieldSuggestions(fields); + } else if (filteredFields && isDeclaringField(activeTyping)) { + autocompleteSuggestions = getFieldSuggestions(filteredFields); } else if (isAccessingProperty(activeTyping)) { const className = activeTyping.substring(0, activeTyping.length - 1).split('.')[0]; autocompleteSuggestions = getClassMemberSuggestions(suggestions, className); } else if (showStaticSuggestions(activeTyping, words, primitives)) { - autocompleteSuggestions = getStaticSuggestions(suggestions, Boolean(fields?.length)); + autocompleteSuggestions = getStaticSuggestions({ suggestions, hasFields, isRuntimeContext }); } return autocompleteSuggestions; }; diff --git a/packages/kbn-monaco/src/painless/worker/lib/autocomplete_utils.test.ts b/packages/kbn-monaco/src/painless/worker/lib/autocomplete_utils.test.ts index d9420719f6923..802fd0073963a 100644 --- a/packages/kbn-monaco/src/painless/worker/lib/autocomplete_utils.test.ts +++ b/packages/kbn-monaco/src/painless/worker/lib/autocomplete_utils.test.ts @@ -23,6 +23,8 @@ import { hasDeclaredType, isAccessingProperty, showStaticSuggestions, + isDefiningString, + isDefiningBoolean, } from './autocomplete_utils'; 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()', () => { test('returns true or false depending if a type is declared or the string contains a "."', () => { expect(showStaticSuggestions('a', ['a'], primitives)).toEqual(true); diff --git a/packages/kbn-monaco/src/painless/worker/lib/autocomplete_utils.ts b/packages/kbn-monaco/src/painless/worker/lib/autocomplete_utils.ts index 7c53d2f8167bd..97a05daf37842 100644 --- a/packages/kbn-monaco/src/painless/worker/lib/autocomplete_utils.ts +++ b/packages/kbn-monaco/src/painless/worker/lib/autocomplete_utils.ts @@ -36,11 +36,39 @@ export const isAccessingProperty = (activeTyping: string): boolean => { /** * If the preceding word is a primitive type, e.g., "boolean", * 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 => { 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 */ @@ -62,8 +90,10 @@ export const isDeclaringField = (activeTyping: string): boolean => { /** * 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 - * 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 + * 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 = ( activeTyping: string, @@ -72,5 +102,10 @@ export const showStaticSuggestions = ( ): boolean => { 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 + ); }; diff --git a/packages/kbn-monaco/src/register_globals.ts b/packages/kbn-monaco/src/register_globals.ts index 630467dd81711..db97b69c013af 100644 --- a/packages/kbn-monaco/src/register_globals.ts +++ b/packages/kbn-monaco/src/register_globals.ts @@ -36,6 +36,7 @@ monaco.languages.setMonarchTokensProvider(XJsonLang.ID, XJsonLang.lexerRules); monaco.languages.setLanguageConfiguration(XJsonLang.ID, XJsonLang.languageConfiguration); monaco.languages.register({ id: PainlessLang.ID }); monaco.languages.setMonarchTokensProvider(PainlessLang.ID, PainlessLang.lexerRules); +monaco.languages.setLanguageConfiguration(PainlessLang.ID, PainlessLang.languageConfiguration); monaco.languages.register({ id: EsqlLang.ID }); monaco.languages.setMonarchTokensProvider(EsqlLang.ID, EsqlLang.lexerRules);