From 58a71c7e9137915e2885001c59e544cceb6e8b52 Mon Sep 17 00:00:00 2001 From: Sarah Zinger Date: Tue, 1 Feb 2022 22:53:32 -0500 Subject: [PATCH] Cloudwatch: Add syntax highlighting and autocomplete for "Metric Search" (#43985) * Create a "monarch" folder with everything you need to do syntax highlighting and autocompletion. * Use this new monarch folder with existing cloudwatch sql. * Add metric math syntax highlighting and autocomplete. * Make autocomplete "smarter": - search always inserts a string as first arg - strings can't contain predefined functions - operators follow the last closing ) * Add some tests for Metric Math's CompletionItemProvider. * Fixes After CR: - refactor CompletionItemProvider, so that it only requires args that are dynamic or outside of it's responsibility - Update and add tests with mocked monaco - Add more autocomplete suggestions for SEARCH expression functions - sort keywords and give different priority from function to make more visually distinctive. * Change QueryEditor to auto-resize and look more like the one in Prometheus. * Add autocomplete for time periods for the third arg of Search. * More CR fixes: - fix missing break - add unit tests for statementPosition - fix broken time period - sort time periods * Bug fix --- .../grafana-ui/src/components/Monaco/theme.ts | 15 +- .../index.ts | 0 .../multiLineFullQuery.ts | 0 ...ultiLineIncompleteQueryWithoutNamespace.ts | 0 .../singleLineEmptyQuery.ts | 0 .../singleLineFullQuery.ts | 0 .../singleLineTwoQueries.ts | 0 .../__mocks__/cloudwatch-sql/Monaco.ts | 38 ----- .../afterFunctionQuery.ts | 28 ++++ .../__mocks__/metric-math-test-data/index.ts | 6 + .../secondArgAfterSearchQuery.ts | 19 +++ .../metric-math-test-data/secondArgQuery.ts | 19 +++ .../singleLineEmptyQuery.ts | 10 ++ .../thirdArgAfterSearchQuery.ts | 22 +++ .../withinStringQuery.ts | 24 +++ .../cloudwatch/__mocks__/monarch/Monaco.ts | 58 +++++++ .../{cloudwatch-sql => monarch}/TextModel.ts | 4 +- .../completion/CompletionItemProvider.ts | 63 +++---- .../completion/linkedTokenBuilder.test.ts | 58 ------- .../completion/statementPosition.test.ts | 20 ++- .../completion/statementPosition.ts | 50 +++--- .../completion/suggestionKind.ts | 2 +- .../completion/tokenUtils.test.ts | 39 +++-- .../cloudwatch-sql/completion/tokenUtils.ts | 10 +- .../cloudwatch-sql/completion/types.ts | 89 ++-------- .../cloudwatch/cloudwatch-sql/definition.ts | 5 +- .../cloudwatch/cloudwatch-sql/register.ts | 19 --- .../components/MathExpressionQueryField.tsx | 90 ++++++++-- .../components/MetricsQueryEditor.tsx | 1 + .../cloudwatch/components/SQLCodeEditor.tsx | 6 +- .../datasource/cloudwatch/datasource.ts | 11 +- .../completion/CompletionItemProvider.test.ts | 72 ++++++++ .../completion/CompletionItemProvider.ts | 123 ++++++++++++++ .../completion/statementPosition.test.ts | 69 ++++++++ .../completion/statementPosition.ts | 54 ++++++ .../metric-math/completion/suggestionKind.ts | 18 ++ .../metric-math/completion/types.ts | 15 ++ .../cloudwatch/metric-math/definition.ts | 10 ++ .../cloudwatch/metric-math/language.ts | 156 ++++++++++++++++++ .../monarch/CompletionItemProvider.ts | 91 ++++++++++ .../completion => monarch}/LinkedToken.ts | 39 +++-- .../completion => monarch}/commands.ts | 0 .../monarch/linkedTokenBuilder.test.ts | 90 ++++++++++ .../linkedTokenBuilder.ts | 19 ++- .../datasource/cloudwatch/monarch/register.ts | 34 ++++ .../datasource/cloudwatch/monarch/types.ts | 100 +++++++++++ 46 files changed, 1261 insertions(+), 335 deletions(-) rename public/app/plugins/datasource/cloudwatch/__mocks__/{cloudwatch-sql/test-data => cloudwatch-sql-test-data}/index.ts (100%) rename public/app/plugins/datasource/cloudwatch/__mocks__/{cloudwatch-sql/test-data => cloudwatch-sql-test-data}/multiLineFullQuery.ts (100%) rename public/app/plugins/datasource/cloudwatch/__mocks__/{cloudwatch-sql/test-data => cloudwatch-sql-test-data}/multiLineIncompleteQueryWithoutNamespace.ts (100%) rename public/app/plugins/datasource/cloudwatch/__mocks__/{cloudwatch-sql/test-data => cloudwatch-sql-test-data}/singleLineEmptyQuery.ts (100%) rename public/app/plugins/datasource/cloudwatch/__mocks__/{cloudwatch-sql/test-data => cloudwatch-sql-test-data}/singleLineFullQuery.ts (100%) rename public/app/plugins/datasource/cloudwatch/__mocks__/{cloudwatch-sql/test-data => cloudwatch-sql-test-data}/singleLineTwoQueries.ts (100%) delete mode 100644 public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-sql/Monaco.ts create mode 100644 public/app/plugins/datasource/cloudwatch/__mocks__/metric-math-test-data/afterFunctionQuery.ts create mode 100644 public/app/plugins/datasource/cloudwatch/__mocks__/metric-math-test-data/index.ts create mode 100644 public/app/plugins/datasource/cloudwatch/__mocks__/metric-math-test-data/secondArgAfterSearchQuery.ts create mode 100644 public/app/plugins/datasource/cloudwatch/__mocks__/metric-math-test-data/secondArgQuery.ts create mode 100644 public/app/plugins/datasource/cloudwatch/__mocks__/metric-math-test-data/singleLineEmptyQuery.ts create mode 100644 public/app/plugins/datasource/cloudwatch/__mocks__/metric-math-test-data/thirdArgAfterSearchQuery.ts create mode 100644 public/app/plugins/datasource/cloudwatch/__mocks__/metric-math-test-data/withinStringQuery.ts create mode 100644 public/app/plugins/datasource/cloudwatch/__mocks__/monarch/Monaco.ts rename public/app/plugins/datasource/cloudwatch/__mocks__/{cloudwatch-sql => monarch}/TextModel.ts (81%) delete mode 100644 public/app/plugins/datasource/cloudwatch/cloudwatch-sql/completion/linkedTokenBuilder.test.ts delete mode 100644 public/app/plugins/datasource/cloudwatch/cloudwatch-sql/register.ts create mode 100644 public/app/plugins/datasource/cloudwatch/metric-math/completion/CompletionItemProvider.test.ts create mode 100644 public/app/plugins/datasource/cloudwatch/metric-math/completion/CompletionItemProvider.ts create mode 100644 public/app/plugins/datasource/cloudwatch/metric-math/completion/statementPosition.test.ts create mode 100644 public/app/plugins/datasource/cloudwatch/metric-math/completion/statementPosition.ts create mode 100644 public/app/plugins/datasource/cloudwatch/metric-math/completion/suggestionKind.ts create mode 100644 public/app/plugins/datasource/cloudwatch/metric-math/completion/types.ts create mode 100644 public/app/plugins/datasource/cloudwatch/metric-math/definition.ts create mode 100644 public/app/plugins/datasource/cloudwatch/metric-math/language.ts create mode 100644 public/app/plugins/datasource/cloudwatch/monarch/CompletionItemProvider.ts rename public/app/plugins/datasource/cloudwatch/{cloudwatch-sql/completion => monarch}/LinkedToken.ts (70%) rename public/app/plugins/datasource/cloudwatch/{cloudwatch-sql/completion => monarch}/commands.ts (100%) create mode 100644 public/app/plugins/datasource/cloudwatch/monarch/linkedTokenBuilder.test.ts rename public/app/plugins/datasource/cloudwatch/{cloudwatch-sql/completion => monarch}/linkedTokenBuilder.ts (77%) create mode 100644 public/app/plugins/datasource/cloudwatch/monarch/register.ts create mode 100644 public/app/plugins/datasource/cloudwatch/monarch/types.ts diff --git a/packages/grafana-ui/src/components/Monaco/theme.ts b/packages/grafana-ui/src/components/Monaco/theme.ts index fb5d911bed1..a85d79ac025 100644 --- a/packages/grafana-ui/src/components/Monaco/theme.ts +++ b/packages/grafana-ui/src/components/Monaco/theme.ts @@ -17,18 +17,27 @@ function getColors(theme?: GrafanaTheme2): monacoTypes.editor.IColors { export default function defineThemes(monaco: Monaco, theme?: GrafanaTheme2) { // color tokens are defined here https://github.com/microsoft/vscode/blob/main/src/vs/platform/theme/common/colorRegistry.ts#L174 const colors = getColors(theme); - monaco.editor.defineTheme('grafana-dark', { base: 'vs-dark', inherit: true, colors: colors, - rules: [], + // fallback syntax highlighting for languages that microsoft doesn't handle (ex cloudwatch's metric math) + rules: [ + { token: 'predefined', foreground: theme?.visualization.getColorByName('purple') }, + { token: 'operator', foreground: theme?.visualization.getColorByName('orange') }, + { token: 'tag', foreground: theme?.visualization.getColorByName('green') }, + ], }); monaco.editor.defineTheme('grafana-light', { base: 'vs', inherit: true, colors: colors, - rules: [], + // fallback syntax highlighting for languages that microsoft doesn't handle (ex cloudwatch's metric math) + rules: [ + { token: 'predefined', foreground: theme?.visualization.getColorByName('purple') }, + { token: 'operator', foreground: theme?.visualization.getColorByName('orange') }, + { token: 'tag', foreground: theme?.visualization.getColorByName('green') }, + ], }); } diff --git a/public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-sql/test-data/index.ts b/public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-sql-test-data/index.ts similarity index 100% rename from public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-sql/test-data/index.ts rename to public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-sql-test-data/index.ts diff --git a/public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-sql/test-data/multiLineFullQuery.ts b/public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-sql-test-data/multiLineFullQuery.ts similarity index 100% rename from public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-sql/test-data/multiLineFullQuery.ts rename to public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-sql-test-data/multiLineFullQuery.ts diff --git a/public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-sql/test-data/multiLineIncompleteQueryWithoutNamespace.ts b/public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-sql-test-data/multiLineIncompleteQueryWithoutNamespace.ts similarity index 100% rename from public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-sql/test-data/multiLineIncompleteQueryWithoutNamespace.ts rename to public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-sql-test-data/multiLineIncompleteQueryWithoutNamespace.ts diff --git a/public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-sql/test-data/singleLineEmptyQuery.ts b/public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-sql-test-data/singleLineEmptyQuery.ts similarity index 100% rename from public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-sql/test-data/singleLineEmptyQuery.ts rename to public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-sql-test-data/singleLineEmptyQuery.ts diff --git a/public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-sql/test-data/singleLineFullQuery.ts b/public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-sql-test-data/singleLineFullQuery.ts similarity index 100% rename from public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-sql/test-data/singleLineFullQuery.ts rename to public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-sql-test-data/singleLineFullQuery.ts diff --git a/public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-sql/test-data/singleLineTwoQueries.ts b/public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-sql-test-data/singleLineTwoQueries.ts similarity index 100% rename from public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-sql/test-data/singleLineTwoQueries.ts rename to public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-sql-test-data/singleLineTwoQueries.ts diff --git a/public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-sql/Monaco.ts b/public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-sql/Monaco.ts deleted file mode 100644 index 5831c3f5396..00000000000 --- a/public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-sql/Monaco.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { monacoTypes } from '@grafana/ui'; -import { Monaco } from '../../cloudwatch-sql/completion/types'; -import { - multiLineFullQuery, - singleLineFullQuery, - singleLineEmptyQuery, - singleLineTwoQueries, - multiLineIncompleteQueryWithoutNamespace, -} from './test-data'; - -const TestData = { - [multiLineFullQuery.query]: multiLineFullQuery.tokens, - [singleLineFullQuery.query]: singleLineFullQuery.tokens, - [singleLineEmptyQuery.query]: singleLineEmptyQuery.tokens, - [singleLineTwoQueries.query]: singleLineTwoQueries.tokens, - [multiLineIncompleteQueryWithoutNamespace.query]: multiLineIncompleteQueryWithoutNamespace.tokens, -}; - -// Stub for the Monaco instance. Only implements the parts that are used in cloudwatch sql -const MonacoMock: Monaco = { - editor: { - tokenize: (value: string, languageId: string) => { - return TestData[value]; - }, - }, - Range: { - containsPosition: (range: monacoTypes.IRange, position: monacoTypes.IPosition) => { - return ( - position.lineNumber >= range.startLineNumber && - position.lineNumber <= range.endLineNumber && - position.column >= range.startColumn && - position.column <= range.endColumn - ); - }, - }, -}; - -export default MonacoMock; diff --git a/public/app/plugins/datasource/cloudwatch/__mocks__/metric-math-test-data/afterFunctionQuery.ts b/public/app/plugins/datasource/cloudwatch/__mocks__/metric-math-test-data/afterFunctionQuery.ts new file mode 100644 index 00000000000..f117dcf299a --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/__mocks__/metric-math-test-data/afterFunctionQuery.ts @@ -0,0 +1,28 @@ +import { monacoTypes } from '@grafana/ui'; + +export const afterFunctionQuery = { + query: 'AVG() ', + tokens: [ + [ + { + offset: 0, + type: 'predefined.cloudwatch-MetricMath', + language: 'cloudwatch-MetricMath', + }, + { + offset: 3, + type: 'delimiter.parenthesis.cloudwatch-MetricMath', + language: 'cloudwatch-MetricMath', + }, + { + offset: 5, + type: 'white.cloudwatch-MetricMath', + language: 'cloudwatch-MetricMath', + }, + ], + ] as monacoTypes.Token[][], + position: { + lineNumber: 1, + column: 7, + }, +}; diff --git a/public/app/plugins/datasource/cloudwatch/__mocks__/metric-math-test-data/index.ts b/public/app/plugins/datasource/cloudwatch/__mocks__/metric-math-test-data/index.ts new file mode 100644 index 00000000000..1d4bcf2dbf1 --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/__mocks__/metric-math-test-data/index.ts @@ -0,0 +1,6 @@ +export { singleLineEmptyQuery } from './singleLineEmptyQuery'; +export { afterFunctionQuery } from './afterFunctionQuery'; +export { secondArgQuery } from './secondArgQuery'; +export { secondArgAfterSearchQuery } from './secondArgAfterSearchQuery'; +export { thirdArgAfterSearchQuery } from './thirdArgAfterSearchQuery'; +export { withinStringQuery } from './withinStringQuery'; diff --git a/public/app/plugins/datasource/cloudwatch/__mocks__/metric-math-test-data/secondArgAfterSearchQuery.ts b/public/app/plugins/datasource/cloudwatch/__mocks__/metric-math-test-data/secondArgAfterSearchQuery.ts new file mode 100644 index 00000000000..9d0ddaaef68 --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/__mocks__/metric-math-test-data/secondArgAfterSearchQuery.ts @@ -0,0 +1,19 @@ +import { monacoTypes } from '@grafana/ui'; + +export const secondArgAfterSearchQuery = { + query: "SEARCH('stuff', )", + tokens: [ + [ + { offset: 0, type: 'predefined.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' }, + { offset: 6, type: 'delimiter.parenthesis.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' }, + { offset: 7, type: 'string.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' }, + { offset: 14, type: 'delimiter.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' }, + { offset: 15, type: 'white.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' }, + { offset: 16, type: 'delimiter.parenthesis.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' }, + ], + ] as monacoTypes.Token[][], + position: { + lineNumber: 1, + column: 18, + }, +}; diff --git a/public/app/plugins/datasource/cloudwatch/__mocks__/metric-math-test-data/secondArgQuery.ts b/public/app/plugins/datasource/cloudwatch/__mocks__/metric-math-test-data/secondArgQuery.ts new file mode 100644 index 00000000000..40a841853bd --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/__mocks__/metric-math-test-data/secondArgQuery.ts @@ -0,0 +1,19 @@ +import { monacoTypes } from '@grafana/ui'; + +export const secondArgQuery = { + query: 'FILL($first, )', + tokens: [ + [ + { offset: 0, type: 'predefined.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' }, + { offset: 4, type: 'delimiter.parenthesis.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' }, + { offset: 5, type: 'variable.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' }, + { offset: 11, type: 'delimiter.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' }, + { offset: 12, type: 'white.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' }, + { offset: 13, type: 'delimiter.parenthesis.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' }, + ], + ] as monacoTypes.Token[][], + position: { + lineNumber: 1, + column: 14, + }, +}; diff --git a/public/app/plugins/datasource/cloudwatch/__mocks__/metric-math-test-data/singleLineEmptyQuery.ts b/public/app/plugins/datasource/cloudwatch/__mocks__/metric-math-test-data/singleLineEmptyQuery.ts new file mode 100644 index 00000000000..2521802a3aa --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/__mocks__/metric-math-test-data/singleLineEmptyQuery.ts @@ -0,0 +1,10 @@ +import { monacoTypes } from '@grafana/ui'; + +export const singleLineEmptyQuery = { + query: '', + tokens: [] as monacoTypes.Token[][], + position: { + lineNumber: 1, + column: 1, + }, +}; diff --git a/public/app/plugins/datasource/cloudwatch/__mocks__/metric-math-test-data/thirdArgAfterSearchQuery.ts b/public/app/plugins/datasource/cloudwatch/__mocks__/metric-math-test-data/thirdArgAfterSearchQuery.ts new file mode 100644 index 00000000000..b68e2a39c08 --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/__mocks__/metric-math-test-data/thirdArgAfterSearchQuery.ts @@ -0,0 +1,22 @@ +import { monacoTypes } from '@grafana/ui'; + +export const thirdArgAfterSearchQuery = { + query: "SEARCH('stuff', 'Average', )", + tokens: [ + [ + { offset: 0, type: 'predefined.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' }, + { offset: 6, type: 'delimiter.parenthesis.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' }, + { offset: 7, type: 'string.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' }, + { offset: 14, type: 'delimiter.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' }, + { offset: 15, type: 'white.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' }, + { offset: 16, type: 'string.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' }, + { offset: 25, type: 'delimiter.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' }, + { offset: 26, type: 'white.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' }, + { offset: 27, type: 'delimiter.parenthesis.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' }, + ], + ] as monacoTypes.Token[][], + position: { + lineNumber: 1, + column: 28, + }, +}; diff --git a/public/app/plugins/datasource/cloudwatch/__mocks__/metric-math-test-data/withinStringQuery.ts b/public/app/plugins/datasource/cloudwatch/__mocks__/metric-math-test-data/withinStringQuery.ts new file mode 100644 index 00000000000..9c939fa4f3b --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/__mocks__/metric-math-test-data/withinStringQuery.ts @@ -0,0 +1,24 @@ +import { monacoTypes } from '@grafana/ui'; + +export const withinStringQuery = { + query: `SEARCH(' {"Custom-Namespace", "Dimension Name With Spaces"}, `, + tokens: [ + [ + { offset: 0, type: 'predefined.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' }, + { offset: 6, type: 'delimiter.parenthesis.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' }, + { offset: 7, type: 'string.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' }, + { offset: 8, type: 'white.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' }, + { offset: 9, type: 'delimiter.curly.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' }, + { offset: 10, type: 'type.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' }, + { offset: 28, type: 'source.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' }, + { offset: 30, type: 'type.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' }, + { offset: 58, type: 'delimiter.curly.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' }, + { offset: 59, type: 'delimiter.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' }, + { offset: 60, type: 'white.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' }, + ], + ] as monacoTypes.Token[][], + position: { + lineNumber: 1, + column: 62, + }, +}; diff --git a/public/app/plugins/datasource/cloudwatch/__mocks__/monarch/Monaco.ts b/public/app/plugins/datasource/cloudwatch/__mocks__/monarch/Monaco.ts new file mode 100644 index 00000000000..aeb175784e0 --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/__mocks__/monarch/Monaco.ts @@ -0,0 +1,58 @@ +import { monacoTypes } from '@grafana/ui'; +import { Monaco } from '../../monarch/types'; +import * as SQLTestData from '../cloudwatch-sql-test-data'; +import * as MetricMathTestData from '../metric-math-test-data'; + +// Stub for the Monaco instance. +const MonacoMock: Monaco = { + editor: { + tokenize: (value: string, languageId: string) => { + if (languageId === 'cloudwatch-sql') { + const TestData = { + [SQLTestData.multiLineFullQuery.query]: SQLTestData.multiLineFullQuery.tokens, + [SQLTestData.singleLineFullQuery.query]: SQLTestData.singleLineFullQuery.tokens, + [SQLTestData.singleLineEmptyQuery.query]: SQLTestData.singleLineEmptyQuery.tokens, + [SQLTestData.singleLineTwoQueries.query]: SQLTestData.singleLineTwoQueries.tokens, + [SQLTestData.multiLineIncompleteQueryWithoutNamespace.query]: + SQLTestData.multiLineIncompleteQueryWithoutNamespace.tokens, + }; + return TestData[value]; + } + if (languageId === 'cloudwatch-MetricMath') { + const TestData = { + [MetricMathTestData.singleLineEmptyQuery.query]: MetricMathTestData.singleLineEmptyQuery.tokens, + [MetricMathTestData.afterFunctionQuery.query]: MetricMathTestData.afterFunctionQuery.tokens, + [MetricMathTestData.secondArgQuery.query]: MetricMathTestData.secondArgQuery.tokens, + [MetricMathTestData.secondArgAfterSearchQuery.query]: MetricMathTestData.secondArgAfterSearchQuery.tokens, + [MetricMathTestData.withinStringQuery.query]: MetricMathTestData.withinStringQuery.tokens, + [MetricMathTestData.thirdArgAfterSearchQuery.query]: MetricMathTestData.thirdArgAfterSearchQuery.tokens, + }; + return TestData[value]; + } + return []; + }, + }, + Range: { + containsPosition: (range: monacoTypes.IRange, position: monacoTypes.IPosition) => { + return ( + position.lineNumber >= range.startLineNumber && + position.lineNumber <= range.endLineNumber && + position.column >= range.startColumn && + position.column <= range.endColumn + ); + }, + fromPositions: (start: monacoTypes.IPosition, end?: monacoTypes.IPosition) => { + return ({} as any) as monacoTypes.Range; + }, + }, + languages: { + CompletionItemInsertTextRule: { + InsertAsSnippet: 4, + }, + CompletionItemKind: { + Function: 1, + }, + }, +}; + +export default MonacoMock; diff --git a/public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-sql/TextModel.ts b/public/app/plugins/datasource/cloudwatch/__mocks__/monarch/TextModel.ts similarity index 81% rename from public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-sql/TextModel.ts rename to public/app/plugins/datasource/cloudwatch/__mocks__/monarch/TextModel.ts index f1a1494b38b..f29ea2a2a29 100644 --- a/public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-sql/TextModel.ts +++ b/public/app/plugins/datasource/cloudwatch/__mocks__/monarch/TextModel.ts @@ -1,6 +1,6 @@ import { monacoTypes } from '@grafana/ui'; -// Stub for monacoTypes.editor.ITextModel. Only implements the parts that are used in cloudwatch sql +// Stub for monacoTypes.editor.ITextModel function TextModel(value: string) { return { getValue: function (eol?: monacoTypes.editor.EndOfLinePreference, preserveBOM?: boolean): string { @@ -13,7 +13,7 @@ function TextModel(value: string) { }, getLineLength: function (lineNumber: number): number { const lines = value.split('\n'); - return lines[lineNumber - 1].trim().length; + return lines[lineNumber - 1].length; }, }; } diff --git a/public/app/plugins/datasource/cloudwatch/cloudwatch-sql/completion/CompletionItemProvider.ts b/public/app/plugins/datasource/cloudwatch/cloudwatch-sql/completion/CompletionItemProvider.ts index b4839e9d6a4..64842ca96b5 100644 --- a/public/app/plugins/datasource/cloudwatch/cloudwatch-sql/completion/CompletionItemProvider.ts +++ b/public/app/plugins/datasource/cloudwatch/cloudwatch-sql/completion/CompletionItemProvider.ts @@ -1,13 +1,9 @@ import type { Monaco, monacoTypes } from '@grafana/ui'; -import { getTemplateSrv, TemplateSrv } from '@grafana/runtime'; import { uniq } from 'lodash'; -import { CloudWatchDatasource } from '../../datasource'; -import { linkedTokenBuilder } from './linkedTokenBuilder'; -import { getSuggestionKinds } from './suggestionKind'; -import { getStatementPosition } from './statementPosition'; -import { TRIGGER_SUGGEST } from './commands'; -import { TokenType, SuggestionKind, CompletionItemPriority, StatementPosition } from './types'; -import { LinkedToken } from './LinkedToken'; +import { TRIGGER_SUGGEST } from '../../monarch/commands'; +import { LinkedToken } from '../../monarch/LinkedToken'; +import { SuggestionKind, CompletionItemPriority, StatementPosition } from '../../monarch/types'; +import { SQLTokenTypes } from './types'; import { BY, FROM, @@ -24,45 +20,30 @@ import { STATISTICS, } from '../language'; import { getMetricNameToken, getNamespaceToken } from './tokenUtils'; +import { CompletionItemProvider } from '../../monarch/CompletionItemProvider'; +import { CloudWatchDatasource } from '../../datasource'; +import { getTemplateSrv, TemplateSrv } from '@grafana/runtime'; +import { getStatementPosition } from './statementPosition'; +import { getSuggestionKinds } from './suggestionKind'; type CompletionItem = monacoTypes.languages.CompletionItem; -export class CompletionItemProvider { +export class SQLCompletionItemProvider extends CompletionItemProvider { region: string; - templateVariables: string[]; - constructor(private datasource: CloudWatchDatasource, private templateSrv: TemplateSrv = getTemplateSrv()) { - this.templateVariables = this.datasource.getVariables(); + constructor(datasource: CloudWatchDatasource, templateSrv: TemplateSrv = getTemplateSrv()) { + super(datasource, templateSrv); this.region = datasource.getActualRegion(); + this.getStatementPosition = getStatementPosition; + this.getSuggestionKinds = getSuggestionKinds; + this.tokenTypes = SQLTokenTypes; } setRegion(region: string) { this.region = region; } - getCompletionProvider(monaco: Monaco) { - return { - triggerCharacters: [' ', '$', ',', '(', "'"], - provideCompletionItems: async (model: monacoTypes.editor.ITextModel, position: monacoTypes.IPosition) => { - const currentToken = linkedTokenBuilder(monaco, model, position); - const statementPosition = getStatementPosition(currentToken); - const suggestionKinds = getSuggestionKinds(statementPosition); - const suggestions = await this.getSuggestions( - monaco, - currentToken, - suggestionKinds, - statementPosition, - position - ); - - return { - suggestions, - }; - }, - }; - } - - private async getSuggestions( + async getSuggestions( monaco: Monaco, currentToken: LinkedToken | null, suggestionKinds: SuggestionKind[], @@ -182,14 +163,14 @@ export class CompletionItemProvider { let dimensionFilter = {}; let labelKeyTokens; if (statementPosition === StatementPosition.SchemaFuncExtraArgument) { - labelKeyTokens = namespaceToken?.getNextUntil(TokenType.Parenthesis, [ - TokenType.Delimiter, - TokenType.Whitespace, + labelKeyTokens = namespaceToken?.getNextUntil(this.tokenTypes.Parenthesis, [ + this.tokenTypes.Delimiter, + this.tokenTypes.Whitespace, ]); } else if (statementPosition === StatementPosition.AfterGroupByKeywords) { - labelKeyTokens = currentToken?.getPreviousUntil(TokenType.Keyword, [ - TokenType.Delimiter, - TokenType.Whitespace, + labelKeyTokens = currentToken?.getPreviousUntil(this.tokenTypes.Keyword, [ + this.tokenTypes.Delimiter, + this.tokenTypes.Whitespace, ]); } dimensionFilter = (labelKeyTokens || []).reduce((acc, curr) => { diff --git a/public/app/plugins/datasource/cloudwatch/cloudwatch-sql/completion/linkedTokenBuilder.test.ts b/public/app/plugins/datasource/cloudwatch/cloudwatch-sql/completion/linkedTokenBuilder.test.ts deleted file mode 100644 index 45655109cdd..00000000000 --- a/public/app/plugins/datasource/cloudwatch/cloudwatch-sql/completion/linkedTokenBuilder.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { monacoTypes } from '@grafana/ui'; -import MonacoMock from '../../__mocks__/cloudwatch-sql/Monaco'; -import TextModel from '../../__mocks__/cloudwatch-sql/TextModel'; -import { multiLineFullQuery, singleLineFullQuery } from '../../__mocks__/cloudwatch-sql/test-data'; -import { linkedTokenBuilder } from './linkedTokenBuilder'; -import { TokenType } from './types'; -import { DESC, SELECT } from '../language'; - -describe('linkedTokenBuilder', () => { - describe('singleLineFullQuery', () => { - const testModel = TextModel(singleLineFullQuery.query); - - it('should add correct references to next LinkedToken', () => { - const position: monacoTypes.IPosition = { lineNumber: 1, column: 0 }; - const current = linkedTokenBuilder(MonacoMock, testModel as monacoTypes.editor.ITextModel, position); - expect(current?.is(TokenType.Keyword, SELECT)).toBeTruthy(); - expect(current?.getNextNonWhiteSpaceToken()?.is(TokenType.Function, 'AVG')).toBeTruthy(); - }); - - it('should add correct references to previous LinkedToken', () => { - const position: monacoTypes.IPosition = { lineNumber: 1, column: singleLineFullQuery.query.length }; - const current = linkedTokenBuilder(MonacoMock, testModel as monacoTypes.editor.ITextModel, position); - expect(current?.is(TokenType.Number, '10')).toBeTruthy(); - expect(current?.getPreviousNonWhiteSpaceToken()?.is(TokenType.Keyword, 'LIMIT')).toBeTruthy(); - expect( - current?.getPreviousNonWhiteSpaceToken()?.getPreviousNonWhiteSpaceToken()?.is(TokenType.Keyword, DESC) - ).toBeTruthy(); - }); - }); - - describe('multiLineFullQuery', () => { - const testModel = TextModel(multiLineFullQuery.query); - - it('should add LinkedToken with whitespace in case empty lines', () => { - const position: monacoTypes.IPosition = { lineNumber: 3, column: 0 }; - const current = linkedTokenBuilder(MonacoMock, testModel as monacoTypes.editor.ITextModel, position); - expect(current).not.toBeNull(); - expect(current?.isWhiteSpace()).toBeTruthy(); - }); - - it('should add correct references to next LinkedToken', () => { - const position: monacoTypes.IPosition = { lineNumber: 1, column: 0 }; - const current = linkedTokenBuilder(MonacoMock, testModel as monacoTypes.editor.ITextModel, position); - expect(current?.is(TokenType.Keyword, SELECT)).toBeTruthy(); - expect(current?.getNextNonWhiteSpaceToken()?.is(TokenType.Function, 'AVG')).toBeTruthy(); - }); - - it('should add correct references to previous LinkedToken even when references spans over multiple lines', () => { - const position: monacoTypes.IPosition = { lineNumber: 6, column: 7 }; - const current = linkedTokenBuilder(MonacoMock, testModel as monacoTypes.editor.ITextModel, position); - expect(current?.is(TokenType.Number, '10')).toBeTruthy(); - expect(current?.getPreviousNonWhiteSpaceToken()?.is(TokenType.Keyword, 'LIMIT')).toBeTruthy(); - expect( - current?.getPreviousNonWhiteSpaceToken()?.getPreviousNonWhiteSpaceToken()?.is(TokenType.Keyword, DESC) - ).toBeTruthy(); - }); - }); -}); diff --git a/public/app/plugins/datasource/cloudwatch/cloudwatch-sql/completion/statementPosition.test.ts b/public/app/plugins/datasource/cloudwatch/cloudwatch-sql/completion/statementPosition.test.ts index efbac6eb7ad..878411e3954 100644 --- a/public/app/plugins/datasource/cloudwatch/cloudwatch-sql/completion/statementPosition.test.ts +++ b/public/app/plugins/datasource/cloudwatch/cloudwatch-sql/completion/statementPosition.test.ts @@ -1,20 +1,28 @@ import { monacoTypes } from '@grafana/ui'; -import MonacoMock from '../../__mocks__/cloudwatch-sql/Monaco'; -import TextModel from '../../__mocks__/cloudwatch-sql/TextModel'; +import MonacoMock from '../../__mocks__/monarch/Monaco'; +import TextModel from '../../__mocks__/monarch/TextModel'; import { multiLineFullQuery, singleLineFullQuery, singleLineEmptyQuery, singleLineTwoQueries, -} from '../../__mocks__/cloudwatch-sql/test-data'; -import { linkedTokenBuilder } from './linkedTokenBuilder'; -import { StatementPosition } from './types'; +} from '../../__mocks__/cloudwatch-sql-test-data'; +import { linkedTokenBuilder } from '../../monarch/linkedTokenBuilder'; +import { StatementPosition } from '../../monarch/types'; import { getStatementPosition } from './statementPosition'; +import cloudWatchSqlLanguageDefinition from '../definition'; +import { SQLTokenTypes } from './types'; describe('statementPosition', () => { function assertPosition(query: string, position: monacoTypes.IPosition, expected: StatementPosition) { const testModel = TextModel(query); - const current = linkedTokenBuilder(MonacoMock, testModel as monacoTypes.editor.ITextModel, position); + const current = linkedTokenBuilder( + MonacoMock, + cloudWatchSqlLanguageDefinition, + testModel as monacoTypes.editor.ITextModel, + position, + SQLTokenTypes + ); const statementPosition = getStatementPosition(current); expect(statementPosition).toBe(expected); } diff --git a/public/app/plugins/datasource/cloudwatch/cloudwatch-sql/completion/statementPosition.ts b/public/app/plugins/datasource/cloudwatch/cloudwatch-sql/completion/statementPosition.ts index ce204d184be..b1af5f11d09 100644 --- a/public/app/plugins/datasource/cloudwatch/cloudwatch-sql/completion/statementPosition.ts +++ b/public/app/plugins/datasource/cloudwatch/cloudwatch-sql/completion/statementPosition.ts @@ -1,16 +1,17 @@ import { AND, ASC, BY, DESC, EQUALS, FROM, GROUP, NOT_EQUALS, ORDER, SCHEMA, SELECT, WHERE } from '../language'; -import { LinkedToken } from './LinkedToken'; -import { StatementPosition, TokenType } from './types'; +import { LinkedToken } from '../../monarch/LinkedToken'; +import { StatementPosition } from '../../monarch/types'; +import { SQLTokenTypes } from './types'; export function getStatementPosition(currentToken: LinkedToken | null): StatementPosition { const previousNonWhiteSpace = currentToken?.getPreviousNonWhiteSpaceToken(); const previousKeyword = currentToken?.getPreviousKeyword(); - const previousIsSlash = currentToken?.getPreviousNonWhiteSpaceToken()?.is(TokenType.Operator, '/'); + const previousIsSlash = currentToken?.getPreviousNonWhiteSpaceToken()?.is(SQLTokenTypes.Operator, '/'); if ( currentToken === null || (currentToken.isWhiteSpace() && currentToken.previous === null) || - (currentToken.is(TokenType.Keyword, SELECT) && currentToken.previous === null) || + (currentToken.is(SQLTokenTypes.Keyword, SELECT) && currentToken.previous === null) || previousIsSlash || (currentToken.isIdentifier() && (previousIsSlash || currentToken?.previous === null)) ) { @@ -22,7 +23,7 @@ export function getStatementPosition(currentToken: LinkedToken | null): Statemen } if ( - (previousNonWhiteSpace?.is(TokenType.Parenthesis, '(') || currentToken?.is(TokenType.Parenthesis, '()')) && + (previousNonWhiteSpace?.is(SQLTokenTypes.Parenthesis, '(') || currentToken?.is(SQLTokenTypes.Parenthesis, '()')) && previousKeyword?.value === SELECT ) { return StatementPosition.AfterSelectFuncFirstArgument; @@ -37,20 +38,20 @@ export function getStatementPosition(currentToken: LinkedToken | null): Statemen } if ( - (previousNonWhiteSpace?.is(TokenType.Parenthesis, '(') || currentToken?.is(TokenType.Parenthesis, '()')) && + (previousNonWhiteSpace?.is(SQLTokenTypes.Parenthesis, '(') || currentToken?.is(SQLTokenTypes.Parenthesis, '()')) && previousKeyword?.value === SCHEMA ) { return StatementPosition.SchemaFuncFirstArgument; } - if (previousKeyword?.value === SCHEMA && previousNonWhiteSpace?.is(TokenType.Delimiter, ',')) { + if (previousKeyword?.value === SCHEMA && previousNonWhiteSpace?.is(SQLTokenTypes.Delimiter, ',')) { return StatementPosition.SchemaFuncExtraArgument; } if ( (previousKeyword?.value === FROM && previousNonWhiteSpace?.isDoubleQuotedString()) || (previousKeyword?.value === FROM && previousNonWhiteSpace?.isVariable()) || - (previousKeyword?.value === SCHEMA && previousNonWhiteSpace?.is(TokenType.Parenthesis, ')')) + (previousKeyword?.value === SCHEMA && previousNonWhiteSpace?.is(SQLTokenTypes.Parenthesis, ')')) ) { return StatementPosition.AfterFrom; } @@ -58,8 +59,8 @@ export function getStatementPosition(currentToken: LinkedToken | null): Statemen if ( previousKeyword?.value === WHERE && (previousNonWhiteSpace?.isKeyword() || - previousNonWhiteSpace?.is(TokenType.Parenthesis, '(') || - previousNonWhiteSpace?.is(TokenType.Operator, AND)) + previousNonWhiteSpace?.is(SQLTokenTypes.Parenthesis, '(') || + previousNonWhiteSpace?.is(SQLTokenTypes.Operator, AND)) ) { return StatementPosition.WhereKey; } @@ -73,51 +74,52 @@ export function getStatementPosition(currentToken: LinkedToken | null): Statemen if ( previousKeyword?.value === WHERE && - (previousNonWhiteSpace?.is(TokenType.Operator, EQUALS) || previousNonWhiteSpace?.is(TokenType.Operator, NOT_EQUALS)) + (previousNonWhiteSpace?.is(SQLTokenTypes.Operator, EQUALS) || + previousNonWhiteSpace?.is(SQLTokenTypes.Operator, NOT_EQUALS)) ) { return StatementPosition.WhereValue; } if ( previousKeyword?.value === WHERE && - (previousNonWhiteSpace?.isString() || previousNonWhiteSpace?.is(TokenType.Parenthesis, ')')) + (previousNonWhiteSpace?.isString() || previousNonWhiteSpace?.is(SQLTokenTypes.Parenthesis, ')')) ) { return StatementPosition.AfterWhereValue; } if ( - previousKeyword?.is(TokenType.Keyword, BY) && - previousKeyword?.getPreviousKeyword()?.is(TokenType.Keyword, GROUP) && - (previousNonWhiteSpace?.is(TokenType.Keyword, BY) || previousNonWhiteSpace?.is(TokenType.Delimiter, ',')) + previousKeyword?.is(SQLTokenTypes.Keyword, BY) && + previousKeyword?.getPreviousKeyword()?.is(SQLTokenTypes.Keyword, GROUP) && + (previousNonWhiteSpace?.is(SQLTokenTypes.Keyword, BY) || previousNonWhiteSpace?.is(SQLTokenTypes.Delimiter, ',')) ) { return StatementPosition.AfterGroupByKeywords; } if ( - previousKeyword?.is(TokenType.Keyword, BY) && - previousKeyword?.getPreviousKeyword()?.is(TokenType.Keyword, GROUP) && + previousKeyword?.is(SQLTokenTypes.Keyword, BY) && + previousKeyword?.getPreviousKeyword()?.is(SQLTokenTypes.Keyword, GROUP) && (previousNonWhiteSpace?.isIdentifier() || previousNonWhiteSpace?.isDoubleQuotedString()) ) { return StatementPosition.AfterGroupBy; } if ( - previousNonWhiteSpace?.is(TokenType.Keyword, BY) && - previousNonWhiteSpace?.getPreviousKeyword()?.is(TokenType.Keyword, ORDER) + previousNonWhiteSpace?.is(SQLTokenTypes.Keyword, BY) && + previousNonWhiteSpace?.getPreviousKeyword()?.is(SQLTokenTypes.Keyword, ORDER) ) { return StatementPosition.AfterOrderByKeywords; } if ( - previousKeyword?.is(TokenType.Keyword, BY) && - previousKeyword?.getPreviousKeyword()?.is(TokenType.Keyword, ORDER) && - previousNonWhiteSpace?.is(TokenType.Parenthesis) && - previousNonWhiteSpace?.getPreviousNonWhiteSpaceToken()?.is(TokenType.Function) + previousKeyword?.is(SQLTokenTypes.Keyword, BY) && + previousKeyword?.getPreviousKeyword()?.is(SQLTokenTypes.Keyword, ORDER) && + previousNonWhiteSpace?.is(SQLTokenTypes.Parenthesis) && + previousNonWhiteSpace?.getPreviousNonWhiteSpaceToken()?.is(SQLTokenTypes.Function) ) { return StatementPosition.AfterOrderByFunction; } - if (previousKeyword?.is(TokenType.Keyword, DESC) || previousKeyword?.is(TokenType.Keyword, ASC)) { + if (previousKeyword?.is(SQLTokenTypes.Keyword, DESC) || previousKeyword?.is(SQLTokenTypes.Keyword, ASC)) { return StatementPosition.AfterOrderByDirection; } diff --git a/public/app/plugins/datasource/cloudwatch/cloudwatch-sql/completion/suggestionKind.ts b/public/app/plugins/datasource/cloudwatch/cloudwatch-sql/completion/suggestionKind.ts index 100bd67ba00..b7bc9c03889 100644 --- a/public/app/plugins/datasource/cloudwatch/cloudwatch-sql/completion/suggestionKind.ts +++ b/public/app/plugins/datasource/cloudwatch/cloudwatch-sql/completion/suggestionKind.ts @@ -1,4 +1,4 @@ -import { StatementPosition, SuggestionKind } from './types'; +import { StatementPosition, SuggestionKind } from '../../monarch/types'; export function getSuggestionKinds(statementPosition: StatementPosition): SuggestionKind[] { switch (statementPosition) { diff --git a/public/app/plugins/datasource/cloudwatch/cloudwatch-sql/completion/tokenUtils.test.ts b/public/app/plugins/datasource/cloudwatch/cloudwatch-sql/completion/tokenUtils.test.ts index d5d63dbc2b0..762162864d5 100644 --- a/public/app/plugins/datasource/cloudwatch/cloudwatch-sql/completion/tokenUtils.test.ts +++ b/public/app/plugins/datasource/cloudwatch/cloudwatch-sql/completion/tokenUtils.test.ts @@ -1,17 +1,18 @@ import { monacoTypes } from '@grafana/ui'; -import { LinkedToken } from './LinkedToken'; -import MonacoMock from '../../__mocks__/cloudwatch-sql/Monaco'; -import TextModel from '../../__mocks__/cloudwatch-sql/TextModel'; +import MonacoMock from '../../__mocks__/monarch/Monaco'; +import TextModel from '../../__mocks__/monarch/TextModel'; import { multiLineFullQuery, singleLineFullQuery, singleLineTwoQueries, multiLineIncompleteQueryWithoutNamespace, -} from '../../__mocks__/cloudwatch-sql/test-data'; -import { linkedTokenBuilder } from './linkedTokenBuilder'; -import { TokenType } from './types'; +} from '../../__mocks__/cloudwatch-sql-test-data'; +import { LinkedToken } from '../../monarch/LinkedToken'; +import { linkedTokenBuilder } from '../../monarch/linkedTokenBuilder'; +import { SQLTokenTypes } from './types'; import { getMetricNameToken, getNamespaceToken, getSelectStatisticToken, getSelectToken } from './tokenUtils'; import { SELECT } from '../language'; +import cloudWatchSqlLanguageDefinition from '../definition'; const getToken = ( query: string, @@ -19,7 +20,13 @@ const getToken = ( invokeFunction: (token: LinkedToken | null) => LinkedToken | null ) => { const testModel = TextModel(query); - const current = linkedTokenBuilder(MonacoMock, testModel as monacoTypes.editor.ITextModel, position); + const current = linkedTokenBuilder( + MonacoMock, + cloudWatchSqlLanguageDefinition, + testModel as monacoTypes.editor.ITextModel, + position, + SQLTokenTypes + ); return invokeFunction(current); }; @@ -33,7 +40,7 @@ describe('tokenUtils', () => { const token = getToken(query, position, getSelectToken); expect(token).not.toBeNull(); expect(token?.value).toBe(SELECT); - expect(token?.type).toBe(TokenType.Keyword); + expect(token?.type).toBe(SQLTokenTypes.Keyword); }); test.each([ @@ -44,7 +51,7 @@ describe('tokenUtils', () => { ])('getSelectToken should return the right token', (query: string, position: monacoTypes.IPosition) => { const token = getToken(query, position, getSelectStatisticToken); expect(token).not.toBeNull(); - expect(token?.type).toBe(TokenType.Function); + expect(token?.type).toBe(SQLTokenTypes.Function); }); test.each([ @@ -58,7 +65,7 @@ describe('tokenUtils', () => { const token = getToken(query, position, getSelectStatisticToken); expect(token).not.toBeNull(); expect(token?.value).toBe(value); - expect(token?.type).toBe(TokenType.Function); + expect(token?.type).toBe(SQLTokenTypes.Function); } ); @@ -73,19 +80,19 @@ describe('tokenUtils', () => { const token = getToken(query, position, getMetricNameToken); expect(token).not.toBeNull(); expect(token?.value).toBe(value); - expect(token?.type).toBe(TokenType.Identifier); + expect(token?.type).toBe(SQLTokenTypes.Identifier); } ); test.each([ - [singleLineFullQuery.query, '"AWS/EC2"', TokenType.Type, { lineNumber: 1, column: 50 }], - [multiLineFullQuery.query, '"AWS/ECS"', TokenType.Type, { lineNumber: 5, column: 10 }], - [singleLineTwoQueries.query, '"AWS/EC2"', TokenType.Type, { lineNumber: 1, column: 30 }], - [singleLineTwoQueries.query, '"AWS/ECS"', TokenType.Type, { lineNumber: 1, column: 185 }], + [singleLineFullQuery.query, '"AWS/EC2"', SQLTokenTypes.Type, { lineNumber: 1, column: 50 }], + [multiLineFullQuery.query, '"AWS/ECS"', SQLTokenTypes.Type, { lineNumber: 5, column: 10 }], + [singleLineTwoQueries.query, '"AWS/EC2"', SQLTokenTypes.Type, { lineNumber: 1, column: 30 }], + [singleLineTwoQueries.query, '"AWS/ECS"', SQLTokenTypes.Type, { lineNumber: 1, column: 185 }], [multiLineIncompleteQueryWithoutNamespace.query, undefined, undefined, { lineNumber: 2, column: 5 }], ])( 'getNamespaceToken should return the right token', - (query: string, value: string | undefined, tokenType: TokenType | undefined, position: monacoTypes.IPosition) => { + (query: string, value: string | undefined, tokenType: string | undefined, position: monacoTypes.IPosition) => { const token = getToken(query, position, getNamespaceToken); expect(token?.value).toBe(value); expect(token?.type).toBe(tokenType); diff --git a/public/app/plugins/datasource/cloudwatch/cloudwatch-sql/completion/tokenUtils.ts b/public/app/plugins/datasource/cloudwatch/cloudwatch-sql/completion/tokenUtils.ts index 3b1ef36fab9..d9a1762efe4 100644 --- a/public/app/plugins/datasource/cloudwatch/cloudwatch-sql/completion/tokenUtils.ts +++ b/public/app/plugins/datasource/cloudwatch/cloudwatch-sql/completion/tokenUtils.ts @@ -1,9 +1,9 @@ -import { LinkedToken } from './LinkedToken'; +import { LinkedToken } from '../../monarch/LinkedToken'; import { FROM, SCHEMA, SELECT } from '../language'; -import { TokenType } from './types'; +import { SQLTokenTypes } from './types'; export const getSelectToken = (currentToken: LinkedToken | null) => - currentToken?.getPreviousOfType(TokenType.Keyword, SELECT) ?? null; + currentToken?.getPreviousOfType(SQLTokenTypes.Keyword, SELECT) ?? null; export const getSelectStatisticToken = (currentToken: LinkedToken | null) => { const assumedStatisticToken = getSelectToken(currentToken)?.getNextNonWhiteSpaceToken(); @@ -18,7 +18,7 @@ export const getMetricNameToken = (currentToken: LinkedToken | null) => { export const getFromKeywordToken = (currentToken: LinkedToken | null) => { const selectToken = getSelectToken(currentToken); - return selectToken?.getNextOfType(TokenType.Keyword, FROM); + return selectToken?.getNextOfType(SQLTokenTypes.Keyword, FROM); }; export const getNamespaceToken = (currentToken: LinkedToken | null) => { @@ -30,7 +30,7 @@ export const getNamespaceToken = (currentToken: LinkedToken | null) => { ) { // schema is not used return nextNonWhiteSpace; - } else if (nextNonWhiteSpace?.isKeyword() && nextNonWhiteSpace.next?.is(TokenType.Parenthesis, '(')) { + } else if (nextNonWhiteSpace?.isKeyword() && nextNonWhiteSpace.next?.is(SQLTokenTypes.Parenthesis, '(')) { // schema is specified const assumedNamespaceToken = nextNonWhiteSpace.next?.next; if (assumedNamespaceToken?.isDoubleQuotedString() || assumedNamespaceToken?.isVariable()) { diff --git a/public/app/plugins/datasource/cloudwatch/cloudwatch-sql/completion/types.ts b/public/app/plugins/datasource/cloudwatch/cloudwatch-sql/completion/types.ts index a0c8dc881f7..535377a600a 100644 --- a/public/app/plugins/datasource/cloudwatch/cloudwatch-sql/completion/types.ts +++ b/public/app/plugins/datasource/cloudwatch/cloudwatch-sql/completion/types.ts @@ -1,76 +1,15 @@ -import { monacoTypes } from '@grafana/ui'; +import { TokenTypes } from '../../monarch/types'; -export enum TokenType { - Parenthesis = 'delimiter.parenthesis.sql', - Whitespace = 'white.sql', - Keyword = 'keyword.sql', - Delimiter = 'delimiter.sql', - Operator = 'operator.sql', - Identifier = 'identifier.sql', - Type = 'type.sql', - Function = 'predefined.sql', - Number = 'number.sql', - String = 'string.sql', - Variable = 'variable.sql', -} - -export enum StatementPosition { - Unknown, - SelectKeyword, - AfterSelectKeyword, - AfterSelectFuncFirstArgument, - AfterFromKeyword, - SchemaFuncFirstArgument, - SchemaFuncExtraArgument, - FromKeyword, - AfterFrom, - WhereKey, - WhereComparisonOperator, - WhereValue, - AfterWhereValue, - AfterGroupByKeywords, - AfterGroupBy, - AfterOrderByKeywords, - AfterOrderByFunction, - AfterOrderByDirection, -} - -export enum SuggestionKind { - SelectKeyword, - FunctionsWithArguments, - Metrics, - FromKeyword, - SchemaKeyword, - Namespaces, - LabelKeys, - WhereKeyword, - GroupByKeywords, - OrderByKeywords, - FunctionsWithoutArguments, - LimitKeyword, - SortOrderDirectionKeyword, - ComparisonOperators, - LabelValues, - LogicalOperators, -} - -export enum CompletionItemPriority { - High = 'a', - MediumHigh = 'd', - Medium = 'g', - MediumLow = 'k', - Low = 'q', -} - -export interface Editor { - tokenize: (value: string, languageId: string) => monacoTypes.Token[][]; -} - -export interface Range { - containsPosition: (range: monacoTypes.IRange, position: monacoTypes.IPosition) => boolean; -} - -export interface Monaco { - editor: Editor; - Range: Range; -} +export const SQLTokenTypes: TokenTypes = { + Parenthesis: 'delimiter.parenthesis.sql', + Whitespace: 'white.sql', + Keyword: 'keyword.sql', + Delimiter: 'delimiter.sql', + Operator: 'operator.sql', + Identifier: 'identifier.sql', + Type: 'type.sql', + Function: 'predefined.sql', + Number: 'number.sql', + String: 'string.sql', + Variable: 'variable.sql', +}; diff --git a/public/app/plugins/datasource/cloudwatch/cloudwatch-sql/definition.ts b/public/app/plugins/datasource/cloudwatch/cloudwatch-sql/definition.ts index 01efaca8b79..3efb4ae7c06 100644 --- a/public/app/plugins/datasource/cloudwatch/cloudwatch-sql/definition.ts +++ b/public/app/plugins/datasource/cloudwatch/cloudwatch-sql/definition.ts @@ -1,7 +1,10 @@ -export default { +import { LanguageDefinition } from '../monarch/register'; + +const cloudWatchSqlLanguageDefinition: LanguageDefinition = { id: 'cloudwatch-sql', extensions: ['.cloudwatchSql'], aliases: ['CloudWatch', 'cloudwatch', 'CloudWatchSQL'], mimetypes: [], loader: () => import('./language'), }; +export default cloudWatchSqlLanguageDefinition; diff --git a/public/app/plugins/datasource/cloudwatch/cloudwatch-sql/register.ts b/public/app/plugins/datasource/cloudwatch/cloudwatch-sql/register.ts deleted file mode 100644 index 67defc00fba..00000000000 --- a/public/app/plugins/datasource/cloudwatch/cloudwatch-sql/register.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Monaco } from '@grafana/ui'; -import { CompletionItemProvider } from './completion/CompletionItemProvider'; -import language from './definition'; - -export const registerLanguage = (monaco: Monaco, sqlCompletionItemProvider: CompletionItemProvider) => { - const { id, loader } = language; - - const languages = monaco.languages.getLanguages(); - if (languages.find((l) => l.id === id)) { - return; - } - - monaco.languages.register({ id }); - loader().then((monarch) => { - monaco.languages.setMonarchTokensProvider(id, monarch.language); - monaco.languages.setLanguageConfiguration(id, monarch.conf); - monaco.languages.registerCompletionItemProvider(id, sqlCompletionItemProvider.getCompletionProvider(monaco)); - }); -}; diff --git a/public/app/plugins/datasource/cloudwatch/components/MathExpressionQueryField.tsx b/public/app/plugins/datasource/cloudwatch/components/MathExpressionQueryField.tsx index e05a4e35af8..aa960f9769b 100644 --- a/public/app/plugins/datasource/cloudwatch/components/MathExpressionQueryField.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/MathExpressionQueryField.tsx @@ -1,28 +1,84 @@ -import React from 'react'; -import { Input } from '@grafana/ui'; +import React, { useCallback, useRef } from 'react'; +import { CodeEditor, Monaco } from '@grafana/ui'; +import language from '../metric-math/definition'; +import { registerLanguage } from '../monarch/register'; +import type * as monacoType from 'monaco-editor/esm/vs/editor/editor.api'; +import { TRIGGER_SUGGEST } from '../monarch/commands'; +import { CloudWatchDatasource } from '../datasource'; export interface Props { onChange: (query: string) => void; onRunQuery: () => void; expression: string; + datasource: CloudWatchDatasource; } -export function MathExpressionQueryField({ expression: query, onChange, onRunQuery }: React.PropsWithChildren) { - const onKeyDown = (event: React.KeyboardEvent) => { - if (event.key === 'Enter' && (event.shiftKey || event.ctrlKey)) { - event.preventDefault(); - onRunQuery(); - } - }; +export function MathExpressionQueryField({ + expression: query, + onChange, + onRunQuery, + datasource, +}: React.PropsWithChildren) { + const containerRef = useRef(null); + const onEditorMount = useCallback( + (editor: monacoType.editor.IStandaloneCodeEditor, monaco: Monaco) => { + editor.onDidFocusEditorText(() => editor.trigger(TRIGGER_SUGGEST.id, TRIGGER_SUGGEST.id, {})); + editor.addCommand(monaco.KeyMod.Shift | monaco.KeyCode.Enter, () => { + const text = editor.getValue(); + onChange(text); + onRunQuery(); + }); + + // auto resizes the editor to be the height of the content it holds + // this code comes from the Prometheus query editor. + // We may wish to consider abstracting it into the grafana/ui repo in the future + const updateElementHeight = () => { + const containerDiv = containerRef.current; + if (containerDiv !== null && editor.getContentHeight() < 200) { + const pixelHeight = editor.getContentHeight(); + containerDiv.style.height = `${pixelHeight}px`; + containerDiv.style.width = '100%'; + const pixelWidth = containerDiv.clientWidth; + editor.layout({ width: pixelWidth, height: pixelHeight }); + } + }; + + editor.onDidContentSizeChange(updateElementHeight); + updateElementHeight(); + }, + [onChange, onRunQuery] + ); return ( - onChange(e.currentTarget.value)} - onKeyDown={onKeyDown} - /> +
+ { + if (value !== query) { + onChange(value); + } + }} + onBeforeEditorMount={(monaco: Monaco) => + registerLanguage(monaco, language, datasource.metricMathCompletionItemProvider) + } + onEditorDidMount={onEditorMount} + /> +
); } diff --git a/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor.tsx b/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor.tsx index 1a9e3b5c4a7..556d35022ed 100644 --- a/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor.tsx @@ -102,6 +102,7 @@ export class MetricsQueryEditor extends PureComponent { onRunQuery={onRunQuery} expression={query.expression ?? ''} onChange={(expression) => this.props.onChange({ ...query, expression })} + datasource={datasource} > )} diff --git a/public/app/plugins/datasource/cloudwatch/components/SQLCodeEditor.tsx b/public/app/plugins/datasource/cloudwatch/components/SQLCodeEditor.tsx index b16f8e99f3f..65bb944ec70 100644 --- a/public/app/plugins/datasource/cloudwatch/components/SQLCodeEditor.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/SQLCodeEditor.tsx @@ -3,8 +3,8 @@ import type * as monacoType from 'monaco-editor/esm/vs/editor/editor.api'; import { CodeEditor, Monaco } from '@grafana/ui'; import { CloudWatchDatasource } from '../datasource'; import language from '../cloudwatch-sql/definition'; -import { TRIGGER_SUGGEST } from '../cloudwatch-sql/completion/commands'; -import { registerLanguage } from '../cloudwatch-sql/register'; +import { TRIGGER_SUGGEST } from '../monarch/commands'; +import { registerLanguage } from '../monarch/register'; export interface Props { region: string; @@ -43,7 +43,7 @@ export const SQLCodeEditor: FunctionComponent = ({ region, sql, onChange, }} showMiniMap={false} showLineNumbers={true} - onBeforeEditorMount={(monaco: Monaco) => registerLanguage(monaco, datasource.sqlCompletionItemProvider)} + onBeforeEditorMount={(monaco: Monaco) => registerLanguage(monaco, language, datasource.sqlCompletionItemProvider)} onEditorDidMount={onEditorMount} /> ); diff --git a/public/app/plugins/datasource/cloudwatch/datasource.ts b/public/app/plugins/datasource/cloudwatch/datasource.ts index 3ff9da21226..a28a851740c 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.ts +++ b/public/app/plugins/datasource/cloudwatch/datasource.ts @@ -58,7 +58,8 @@ import { increasingInterval } from './utils/rxjs/increasingInterval'; import { toTestingStatus } from '@grafana/runtime/src/utils/queryResponse'; import { addDataLinksToLogsResponse } from './utils/datalinks'; import { runWithRetry } from './utils/logsRetry'; -import { CompletionItemProvider } from './cloudwatch-sql/completion/CompletionItemProvider'; +import { SQLCompletionItemProvider } from './cloudwatch-sql/completion/CompletionItemProvider'; +import { MetricMathCompletionItemProvider } from './metric-math/completion/CompletionItemProvider'; const DS_QUERY_ENDPOINT = '/api/ds/query'; @@ -89,7 +90,10 @@ export class CloudWatchDatasource defaultRegion: any; datasourceName: string; languageProvider: CloudWatchLanguageProvider; - sqlCompletionItemProvider: CompletionItemProvider; + sqlCompletionItemProvider: SQLCompletionItemProvider; + + metricMathCompletionItemProvider: MetricMathCompletionItemProvider; + tracingDataSourceUid?: string; logsTimeout: string; @@ -118,7 +122,8 @@ export class CloudWatchDatasource this.languageProvider = new CloudWatchLanguageProvider(this); this.tracingDataSourceUid = instanceSettings.jsonData.tracingDatasourceUid; this.logsTimeout = instanceSettings.jsonData.logsTimeout || '15m'; - this.sqlCompletionItemProvider = new CompletionItemProvider(this); + this.sqlCompletionItemProvider = new SQLCompletionItemProvider(this, this.templateSrv); + this.metricMathCompletionItemProvider = new MetricMathCompletionItemProvider(this, this.templateSrv); } query(options: DataQueryRequest): Observable { diff --git a/public/app/plugins/datasource/cloudwatch/metric-math/completion/CompletionItemProvider.test.ts b/public/app/plugins/datasource/cloudwatch/metric-math/completion/CompletionItemProvider.test.ts new file mode 100644 index 00000000000..e7587d072b2 --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/metric-math/completion/CompletionItemProvider.test.ts @@ -0,0 +1,72 @@ +import MonacoMock from '../../__mocks__/monarch/Monaco'; +import TextModel from '../../__mocks__/monarch/TextModel'; +import { MetricMathCompletionItemProvider } from './CompletionItemProvider'; +import { getTemplateSrv } from '@grafana/runtime'; +import { CloudWatchDatasource } from '../../datasource'; +import cloudWatchMetricMathLanguageDefinition from '../definition'; +import { Monaco, monacoTypes } from '@grafana/ui'; +import { IPosition } from 'monaco-editor'; +import { + METRIC_MATH_FNS, + METRIC_MATH_KEYWORDS, + METRIC_MATH_OPERATORS, + METRIC_MATH_PERIODS, + METRIC_MATH_STATISTIC_KEYWORD_STRINGS, +} from '../language'; +import * as MetricMathTestData from '../../__mocks__/metric-math-test-data'; + +const getSuggestions = async (value: string, position: IPosition) => { + const setup = new MetricMathCompletionItemProvider( + ({ + getVariables: () => [], + getActualRegion: () => 'us-east-2', + } as any) as CloudWatchDatasource, + getTemplateSrv() + ); + const monaco = MonacoMock as Monaco; + const provider = setup.getCompletionProvider(monaco, cloudWatchMetricMathLanguageDefinition); + const { suggestions } = await provider.provideCompletionItems( + TextModel(value) as monacoTypes.editor.ITextModel, + position + ); + return suggestions; +}; +describe('MetricMath: CompletionItemProvider', () => { + describe('getSuggestions', () => { + it('returns a suggestion for every metric math function when the input field is empty', async () => { + const { query, position } = MetricMathTestData.singleLineEmptyQuery; + const suggestions = await getSuggestions(query, position); + expect(suggestions.length).toEqual(METRIC_MATH_FNS.length); + }); + + it('returns a suggestion for every metric math operator when at the end of a function', async () => { + const { query, position } = MetricMathTestData.afterFunctionQuery; + const suggestions = await getSuggestions(query, position); + expect(suggestions.length).toEqual(METRIC_MATH_OPERATORS.length); + }); + + it('returns a suggestion for every metric math function and keyword if at the start of the second argument of a function', async () => { + const { query, position } = MetricMathTestData.secondArgQuery; + const suggestions = await getSuggestions(query, position); + expect(suggestions.length).toEqual(METRIC_MATH_FNS.length + METRIC_MATH_KEYWORDS.length); + }); + + it('does not have any particular suggestions if within a string', async () => { + const { query, position } = MetricMathTestData.withinStringQuery; + const suggestions = await getSuggestions(query, position); + expect(suggestions.length).toEqual(0); + }); + + it('returns a suggestion for every statistic if the second arg of a search function', async () => { + const { query, position } = MetricMathTestData.secondArgAfterSearchQuery; + const suggestions = await getSuggestions(query, position); + expect(suggestions.length).toEqual(METRIC_MATH_STATISTIC_KEYWORD_STRINGS.length); + }); + + it('returns a suggestion for every period if the third arg of a search function', async () => { + const { query, position } = MetricMathTestData.thirdArgAfterSearchQuery; + const suggestions = await getSuggestions(query, position); + expect(suggestions.length).toEqual(METRIC_MATH_PERIODS.length); + }); + }); +}); diff --git a/public/app/plugins/datasource/cloudwatch/metric-math/completion/CompletionItemProvider.ts b/public/app/plugins/datasource/cloudwatch/metric-math/completion/CompletionItemProvider.ts new file mode 100644 index 00000000000..20b86ff0f16 --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/metric-math/completion/CompletionItemProvider.ts @@ -0,0 +1,123 @@ +import type { Monaco, monacoTypes } from '@grafana/ui'; +import { TRIGGER_SUGGEST } from '../../monarch/commands'; +import { SuggestionKind, CompletionItemPriority, StatementPosition } from '../../monarch/types'; +import { LinkedToken } from '../../monarch/LinkedToken'; +import { + METRIC_MATH_FNS, + METRIC_MATH_KEYWORDS, + METRIC_MATH_OPERATORS, + METRIC_MATH_PERIODS, + METRIC_MATH_STATISTIC_KEYWORD_STRINGS, +} from '../language'; +import { CompletionItemProvider } from '../../monarch/CompletionItemProvider'; +import { MetricMathTokenTypes } from './types'; +import { CloudWatchDatasource } from '../../datasource'; +import { getTemplateSrv, TemplateSrv } from '@grafana/runtime'; +import { getStatementPosition } from './statementPosition'; +import { getSuggestionKinds } from './suggestionKind'; + +type CompletionItem = monacoTypes.languages.CompletionItem; + +export class MetricMathCompletionItemProvider extends CompletionItemProvider { + constructor(datasource: CloudWatchDatasource, templateSrv: TemplateSrv = getTemplateSrv()) { + super(datasource, templateSrv); + this.getStatementPosition = getStatementPosition; + this.getSuggestionKinds = getSuggestionKinds; + this.tokenTypes = MetricMathTokenTypes; + } + + async getSuggestions( + monaco: Monaco, + currentToken: LinkedToken | null, + suggestionKinds: SuggestionKind[], + statementPosition: StatementPosition, + position: monacoTypes.IPosition + ): Promise { + let suggestions: CompletionItem[] = []; + const invalidRangeToken = currentToken?.isWhiteSpace() || currentToken?.isParenthesis(); + const range = + invalidRangeToken || !currentToken?.range ? monaco.Range.fromPositions(position) : currentToken?.range; + + const toCompletionItem = (value: string, rest: Partial = {}) => { + const item: CompletionItem = { + label: value, + insertText: value, + kind: monaco.languages.CompletionItemKind.Field, + range, + sortText: CompletionItemPriority.Medium, + ...rest, + }; + return item; + }; + + function addSuggestion(value: string, rest: Partial = {}) { + suggestions = [...suggestions, toCompletionItem(value, rest)]; + } + + for (const suggestion of suggestionKinds) { + switch (suggestion) { + case SuggestionKind.FunctionsWithArguments: + METRIC_MATH_FNS.map((f) => + addSuggestion(f, { + insertText: f === 'SEARCH' ? `${f}('$0')` : `${f}($0)`, + insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + command: TRIGGER_SUGGEST, + kind: monaco.languages.CompletionItemKind.Function, + }) + ); + break; + + case SuggestionKind.KeywordArguments: + METRIC_MATH_KEYWORDS.map((s) => + addSuggestion(s, { + insertText: s, + command: TRIGGER_SUGGEST, + kind: monaco.languages.CompletionItemKind.Keyword, + sortText: CompletionItemPriority.MediumHigh, + }) + ); + break; + + case SuggestionKind.Statistic: + METRIC_MATH_STATISTIC_KEYWORD_STRINGS.map((s) => + addSuggestion(s, { + insertText: `'${s}', `, + command: TRIGGER_SUGGEST, + }) + ); + break; + + case SuggestionKind.Operators: + METRIC_MATH_OPERATORS.map((s) => + addSuggestion(s, { + insertText: `${s} `, + command: TRIGGER_SUGGEST, + }) + ); + break; + + case SuggestionKind.Period: + METRIC_MATH_PERIODS.map((s, idx) => + addSuggestion(s.toString(), { + kind: monaco.languages.CompletionItemKind.Value, + sortText: String.fromCharCode(97 + idx), // converts index 0, 1 to "a", "b", etc needed to show the time periods in numerical order + }) + ); + break; + } + } + + // always suggest template variables + this.templateVariables.map((v) => { + addSuggestion(v, { + range, + label: v, + insertText: v, + kind: monaco.languages.CompletionItemKind.Variable, + sortText: CompletionItemPriority.Low, + }); + }); + + return suggestions; + } +} diff --git a/public/app/plugins/datasource/cloudwatch/metric-math/completion/statementPosition.test.ts b/public/app/plugins/datasource/cloudwatch/metric-math/completion/statementPosition.test.ts new file mode 100644 index 00000000000..a4b32712015 --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/metric-math/completion/statementPosition.test.ts @@ -0,0 +1,69 @@ +import { monacoTypes } from '@grafana/ui'; +import MonacoMock from '../../__mocks__/monarch/Monaco'; +import TextModel from '../../__mocks__/monarch/TextModel'; +import * as MetricMathTestQueries from '../../__mocks__/metric-math-test-data'; +import { linkedTokenBuilder } from '../../monarch/linkedTokenBuilder'; +import { StatementPosition } from '../../monarch/types'; +import { getStatementPosition } from './statementPosition'; +import cloudWatchSqlLanguageDefinition from '../definition'; +import { MetricMathTokenTypes } from './types'; + +describe('statementPosition', () => { + function createToken(query: string, position: monacoTypes.IPosition) { + const testModel = TextModel(query); + return linkedTokenBuilder( + MonacoMock, + cloudWatchSqlLanguageDefinition, + testModel as monacoTypes.editor.ITextModel, + position, + MetricMathTokenTypes + ); + } + + it('returns PredefinedFunction when at the beginning of an empty query', () => { + const token = createToken( + MetricMathTestQueries.singleLineEmptyQuery.query, + MetricMathTestQueries.singleLineEmptyQuery.position + ); + expect(getStatementPosition(token)).toEqual(StatementPosition.PredefinedFunction); + }); + + it('returns PredefinedFuncSecondArg when in the second arg of a predefined function', () => { + const token = createToken( + MetricMathTestQueries.secondArgQuery.query, + MetricMathTestQueries.secondArgQuery.position + ); + expect(getStatementPosition(token)).toEqual(StatementPosition.PredefinedFuncSecondArg); + }); + + it('returns SearchFuncSecondArg when in the second arg of a Search function', () => { + const token = createToken( + MetricMathTestQueries.secondArgAfterSearchQuery.query, + MetricMathTestQueries.secondArgAfterSearchQuery.position + ); + expect(getStatementPosition(token)).toEqual(StatementPosition.SearchFuncSecondArg); + }); + + it('returns SearchFuncThirdArg when in the third arg of a Search function', () => { + const token = createToken( + MetricMathTestQueries.thirdArgAfterSearchQuery.query, + MetricMathTestQueries.thirdArgAfterSearchQuery.position + ); + expect(getStatementPosition(token)).toEqual(StatementPosition.SearchFuncThirdArg); + }); + it('returns AfterFunction when after a function', () => { + const token = createToken( + MetricMathTestQueries.afterFunctionQuery.query, + MetricMathTestQueries.afterFunctionQuery.position + ); + expect(getStatementPosition(token)).toEqual(StatementPosition.AfterFunction); + }); + + it('returns WithinString when within a string', () => { + const token = createToken( + MetricMathTestQueries.withinStringQuery.query, + MetricMathTestQueries.withinStringQuery.position + ); + expect(getStatementPosition(token)).toEqual(StatementPosition.WithinString); + }); +}); diff --git a/public/app/plugins/datasource/cloudwatch/metric-math/completion/statementPosition.ts b/public/app/plugins/datasource/cloudwatch/metric-math/completion/statementPosition.ts new file mode 100644 index 00000000000..2e86bb446e4 --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/metric-math/completion/statementPosition.ts @@ -0,0 +1,54 @@ +import { LinkedToken } from '../../monarch/LinkedToken'; +import { StatementPosition } from '../../monarch/types'; +import { MetricMathTokenTypes } from './types'; + +export function getStatementPosition(currentToken: LinkedToken | null): StatementPosition { + const previousNonWhiteSpace = currentToken?.getPreviousNonWhiteSpaceToken(); + + if (currentToken && currentToken.isString()) { + return StatementPosition.WithinString; + } + + if (currentToken && previousNonWhiteSpace) { + const currentFunction = currentToken.getPreviousOfType(MetricMathTokenTypes.Function); + const isAfterComma = previousNonWhiteSpace.is(MetricMathTokenTypes.Delimiter, ','); + const isWithinSearch = currentFunction && currentFunction.value === 'SEARCH'; + const allTokensAfterStartOfSearch = + currentToken.getPreviousUntil(MetricMathTokenTypes.Function, [], 'SEARCH') || []; + + if (isWithinSearch) { + // if there's only one ' then we're still within the first arg + if (allTokensAfterStartOfSearch.filter(({ value }) => value === "'").length === 1) { + return StatementPosition.WithinString; + } + + // if there was a , before the last , and it happened after the start of SEARCH + const lastComma = previousNonWhiteSpace.getPreviousOfType(MetricMathTokenTypes.Delimiter, ','); + if (lastComma) { + const lastCommaIsAfterSearch = + lastComma.range.startColumn > currentFunction.range.startColumn && + lastComma.range.startLineNumber >= currentFunction.range.startLineNumber; + if (lastCommaIsAfterSearch) { + return StatementPosition.SearchFuncThirdArg; + } + } + + // otherwise assume it's the second arg + return StatementPosition.SearchFuncSecondArg; + } + + if (!isWithinSearch && isAfterComma) { + return StatementPosition.PredefinedFuncSecondArg; + } + } + + if (previousNonWhiteSpace?.endsWith(')')) { + return StatementPosition.AfterFunction; + } + + if (!currentToken || !currentToken.isString()) { + return StatementPosition.PredefinedFunction; + } + + return StatementPosition.Unknown; +} diff --git a/public/app/plugins/datasource/cloudwatch/metric-math/completion/suggestionKind.ts b/public/app/plugins/datasource/cloudwatch/metric-math/completion/suggestionKind.ts new file mode 100644 index 00000000000..f74cdbdcdd6 --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/metric-math/completion/suggestionKind.ts @@ -0,0 +1,18 @@ +import { StatementPosition, SuggestionKind } from '../../monarch/types'; + +export function getSuggestionKinds(statementPosition: StatementPosition): SuggestionKind[] { + switch (statementPosition) { + case StatementPosition.PredefinedFunction: + return [SuggestionKind.FunctionsWithArguments]; + case StatementPosition.PredefinedFuncSecondArg: + return [SuggestionKind.FunctionsWithArguments, SuggestionKind.KeywordArguments]; + case StatementPosition.AfterFunction: + return [SuggestionKind.Operators]; + case StatementPosition.SearchFuncSecondArg: + return [SuggestionKind.Statistic]; + case StatementPosition.SearchFuncThirdArg: + return [SuggestionKind.Period]; + } + + return []; +} diff --git a/public/app/plugins/datasource/cloudwatch/metric-math/completion/types.ts b/public/app/plugins/datasource/cloudwatch/metric-math/completion/types.ts new file mode 100644 index 00000000000..7898ab036cf --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/metric-math/completion/types.ts @@ -0,0 +1,15 @@ +import { TokenTypes } from '../../monarch/types'; + +export const MetricMathTokenTypes: TokenTypes = { + Parenthesis: 'delimiter.parenthesis.cloudwatch-MetricMath', + Whitespace: 'white.cloudwatch-MetricMath', + Keyword: 'keyword.cloudwatch-MetricMath', + Delimiter: 'delimiter.cloudwatch-MetricMath', + Operator: 'operator.cloudwatch-MetricMath', + Identifier: 'identifier.cloudwatch-MetricMath', + Type: 'type.cloudwatch-MetricMath', + Function: 'predefined.cloudwatch-MetricMath', + Number: 'number.cloudwatch-MetricMath', + String: 'string.cloudwatch-MetricMath', + Variable: 'variable.cloudwatch-MetricMath', +}; diff --git a/public/app/plugins/datasource/cloudwatch/metric-math/definition.ts b/public/app/plugins/datasource/cloudwatch/metric-math/definition.ts new file mode 100644 index 00000000000..01a0f09c338 --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/metric-math/definition.ts @@ -0,0 +1,10 @@ +import { LanguageDefinition } from '../monarch/register'; + +const cloudWatchMetricMathLanguageDefinition: LanguageDefinition = { + id: 'cloudwatch-MetricMath', + extensions: [], + aliases: [], + mimetypes: [], + loader: () => import('./language'), +}; +export default cloudWatchMetricMathLanguageDefinition; diff --git a/public/app/plugins/datasource/cloudwatch/metric-math/language.ts b/public/app/plugins/datasource/cloudwatch/metric-math/language.ts new file mode 100644 index 00000000000..e3b2cc9bd61 --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/metric-math/language.ts @@ -0,0 +1,156 @@ +import type * as monacoType from 'monaco-editor/esm/vs/editor/editor.api'; + +// Metric Math: https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/using-metric-math.html +export const METRIC_MATH_FNS = [ + 'ABS', + 'ANOMALY_DETECTION_BAND', + 'AVG', + 'CEIL', + 'DATAPOINT_COUNT', + 'DIFF', + 'DIFF_TIME', + 'FILL', + 'FIRST', + 'LAST', + 'FLOOR', + 'IF', + 'INSIGHT_RULE_METRIC', + 'LOG', + 'LOG10', + 'MAX', + 'METRIC_COUNT', + 'METRICS', + 'MIN', + 'MINUTE', + 'HOUR', + 'DAY', + 'DATE', + 'MONTH', + 'YEAR', + 'EPOCH', + 'PERIOD', + 'RATE', + 'REMOVE_EMPTY', + 'RUNNING_SUM', + 'SEARCH', + 'SERVICE_QUOTA', + 'SLICE', + 'SORT', + 'STDDEV', + 'SUM', + 'TIME_SERIES', +]; + +export const METRIC_MATH_STATISTIC_KEYWORD_STRINGS = ['Average', 'Maximum', 'Minimum', 'Sum', 'SampleCount']; // second arguments to SEARCH function + +export const METRIC_MATH_KEYWORDS = ['REPEAT', 'LINEAR', 'ASC', 'DSC']; // standalone magic arguments to functions + +export const METRIC_MATH_OPERATORS = [ + '+', + '-', + '*', + '/', + '^', + '==', + '!=', + '<=', + '>=', + '<', + '>', + 'AND', + '&&', + 'OR', + '||', +]; + +export const METRIC_MATH_PERIODS = [10, 60, 300, 900, 3000, 21600, 86400]; + +export const language: monacoType.languages.IMonarchLanguage = { + id: 'metricMath', + ignoreCase: false, + brackets: [ + { open: '[', close: ']', token: 'delimiter.square' }, + { open: '(', close: ')', token: 'delimiter.parenthesis' }, + { open: '{', close: '}', token: 'delimiter.curly' }, + ], + tokenizer: { + root: [{ include: '@nonNestableStates' }, { include: '@strings' }], + nonNestableStates: [ + { include: '@variables' }, + { include: '@whitespace' }, + { include: '@numbers' }, + { include: '@assignment' }, + { include: '@keywords' }, + { include: '@operators' }, + { include: '@builtInFunctions' }, + [/[;,.]/, 'delimiter'], + [/[(){}\[\]]/, '@brackets'], // [], (), {} are all brackets + ], + keywords: [[METRIC_MATH_KEYWORDS.map(escapeRegExp).join('|'), 'keyword']], + operators: [[METRIC_MATH_OPERATORS.map(escapeRegExp).join('|'), 'operator']], + builtInFunctions: [[METRIC_MATH_FNS.map(escapeRegExp).join('|'), 'predefined']], + variables: [ + [/\$[a-zA-Z0-9-_]+/, 'variable'], // $ followed by any letter/number we assume could be grafana template variable + ], + whitespace: [[/\s+/, 'white']], + assignment: [[/=/, 'tag']], + numbers: [ + [/0[xX][0-9a-fA-F]*/, 'number'], + [/[$][+-]*\d*(\.\d*)?/, 'number'], + [/((\d+(\.\d*)?)|(\.\d+))([eE][\-+]?\d+)?/, 'number'], + ], + // states that start other states (aka nested states): + strings: [ + [/'/, { token: 'string', next: '@string' }], + [/"/, { token: 'type', next: '@string_double' }], + ], + string: [ + [/{/, { token: 'delimiter.curly', next: '@nestedCurly' }], // escape out of string and into nestedCurly + [/\(/, { token: 'delimiter.parenthesis', next: '@nestedParens' }], // escape out of string and into nestedCurly + [/"/, { token: 'type', next: '@string_double' }], // jump into double string + [/'/, { token: 'string', next: '@pop' }], // stop being a string + { include: '@nonNestableStates' }, + [/[^']/, 'string'], // anything that is not a quote, is marked as string + ], + string_double: [ + [/[^"]/, 'type'], // mark anything not a quote as a "type" (different type of string for visual difference) + [/"/, { token: 'type', next: '@pop' }], // mark also as a type and stop being in the double string state + ], + nestedCurly: [ + [/}/, { token: 'delimiter.curly', next: '@pop' }], // escape out of string and into braces + [/'/, { token: 'string', next: '@string' }], // go to string if see start of string + [/"/, { token: 'type', next: '@string_double' }], // go to string_double if see start of double string + ], + nestedParens: [ + [/\)/, { token: 'delimiter.parenthesis', next: '@pop' }], // escape out of string and into braces + [/'/, { token: 'string', next: '@string' }], // go to string if see start of string + [/"/, { token: 'type', next: '@string_double' }], // go to string_double if see start of double string + ], + }, +}; + +export const conf: monacoType.languages.LanguageConfiguration = { + brackets: [ + ['{', '}'], + ['[', ']'], + ['(', ')'], + ], + autoClosingPairs: [ + { open: '{', close: '}' }, + { open: '[', close: ']' }, + { open: '(', close: ')' }, + { open: '"', close: '"' }, + { open: "'", close: "'" }, + ], + surroundingPairs: [ + { open: '{', close: '}' }, + { open: '[', close: ']' }, + { open: '(', close: ')' }, + { open: '"', close: '"' }, + { open: "'", close: "'" }, + ], +}; + +function escapeRegExp(string: string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string +} diff --git a/public/app/plugins/datasource/cloudwatch/monarch/CompletionItemProvider.ts b/public/app/plugins/datasource/cloudwatch/monarch/CompletionItemProvider.ts new file mode 100644 index 00000000000..06404442925 --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/monarch/CompletionItemProvider.ts @@ -0,0 +1,91 @@ +import type { Monaco, monacoTypes } from '@grafana/ui'; +import { getTemplateSrv, TemplateSrv } from '@grafana/runtime'; +import { CloudWatchDatasource } from '../datasource'; +import { linkedTokenBuilder } from './linkedTokenBuilder'; + +import { LinkedToken } from './LinkedToken'; +import { LanguageDefinition } from './register'; +import { StatementPosition, SuggestionKind, TokenTypes } from './types'; + +type CompletionItem = monacoTypes.languages.CompletionItem; + +/* +CompletionItemProvider is an extendable class which needs to implement : +- tokenTypes +- getStatementPosition +- getSuggestionKinds +- getSuggestions +*/ +export class CompletionItemProvider { + templateVariables: string[]; + datasource: CloudWatchDatasource; + templateSrv: TemplateSrv; + tokenTypes: TokenTypes; + + constructor(datasource: CloudWatchDatasource, templateSrv: TemplateSrv = getTemplateSrv()) { + this.datasource = datasource; + this.templateSrv = templateSrv; + this.templateVariables = this.datasource.getVariables(); + this.templateSrv = templateSrv; + + // implement with more specific tokens when extending this class + this.tokenTypes = { + Parenthesis: 'delimiter.parenthesis', + Whitespace: 'white', + Keyword: 'keyword', + Delimiter: 'delimiter', + Operator: 'operator', + Identifier: 'identifier', + Type: 'type', + Function: 'predefined', + Number: 'number', + String: 'string', + Variable: 'variable', + }; + } + + // implemented by subclasses, given a token, returns a lexical position in a query + getStatementPosition(currentToken: LinkedToken | null): StatementPosition { + return StatementPosition.Unknown; + } + + // implemented by subclasses, given a lexical statement position, returns potential kinds of suggestions + getSuggestionKinds(position: StatementPosition): SuggestionKind[] { + return []; + } + + // implemented by subclasses, given potential suggestions kinds, returns suggestion objects for monaco aka "CompletionItem" + getSuggestions( + monaco: Monaco, + currentToken: LinkedToken | null, + suggestionKinds: SuggestionKind[], + statementPosition: StatementPosition, + position: monacoTypes.IPosition + ): Promise { + return Promise.reject([]); + } + + // called by registerLanguage and passed to monaco with registerCompletionItemProvider + // returns an object that implements https://microsoft.github.io/monaco-editor/api/interfaces/monaco.languages.CompletionItemProvider.html + getCompletionProvider(monaco: Monaco, languageDefinition: LanguageDefinition) { + return { + triggerCharacters: [' ', '$', ',', '(', "'"], // one of these characters indicates that it is time to look for a suggestion + provideCompletionItems: async (model: monacoTypes.editor.ITextModel, position: monacoTypes.IPosition) => { + const currentToken = linkedTokenBuilder(monaco, languageDefinition, model, position, this.tokenTypes); + const statementPosition = this.getStatementPosition(currentToken); + const suggestionKinds = this.getSuggestionKinds(statementPosition); + const suggestions = await this.getSuggestions( + monaco, + currentToken, + suggestionKinds, + statementPosition, + position + ); + + return { + suggestions, + }; + }, + }; + } +} diff --git a/public/app/plugins/datasource/cloudwatch/cloudwatch-sql/completion/LinkedToken.ts b/public/app/plugins/datasource/cloudwatch/monarch/LinkedToken.ts similarity index 70% rename from public/app/plugins/datasource/cloudwatch/cloudwatch-sql/completion/LinkedToken.ts rename to public/app/plugins/datasource/cloudwatch/monarch/LinkedToken.ts index 88cd9ee1880..227057efecf 100644 --- a/public/app/plugins/datasource/cloudwatch/cloudwatch-sql/completion/LinkedToken.ts +++ b/public/app/plugins/datasource/cloudwatch/monarch/LinkedToken.ts @@ -1,5 +1,5 @@ import { monacoTypes } from '@grafana/ui'; -import { TokenType } from './types'; +import { TokenTypes } from './types'; export class LinkedToken { constructor( @@ -7,46 +7,55 @@ export class LinkedToken { public value: string, public range: monacoTypes.IRange, public previous: LinkedToken | null, - public next: LinkedToken | null + public next: LinkedToken | null, + public tokenTypes: TokenTypes ) {} isKeyword(): boolean { - return this.type === TokenType.Keyword; + return this.type === this.tokenTypes.Keyword; } isWhiteSpace(): boolean { - return this.type === TokenType.Whitespace; + return this.type === this.tokenTypes.Whitespace; } isParenthesis(): boolean { - return this.type === TokenType.Parenthesis; + return this.type === this.tokenTypes.Parenthesis; } isIdentifier(): boolean { - return this.type === TokenType.Identifier; + return this.type === this.tokenTypes.Identifier; } isString(): boolean { - return this.type === TokenType.String; + return this.type === this.tokenTypes.String; } isDoubleQuotedString(): boolean { - return this.type === TokenType.Type; + return this.type === this.tokenTypes.Type; } isVariable(): boolean { - return this.type === TokenType.Variable; + return this.type === this.tokenTypes.Variable; } isFunction(): boolean { - return this.type === TokenType.Function; + return this.type === this.tokenTypes.Function; } - is(type: TokenType, value?: string | number | boolean): boolean { + isNumber(): boolean { + return this.type === this.tokenTypes.Number; + } + + is(type: string, value?: string | number | boolean): boolean { const isType = this.type === type; return value !== undefined ? isType && this.value === value : isType; } + endsWith(value: string | number | boolean): boolean { + return this.value === value || this.value[this.value.length - 1] === value; + } + getPreviousNonWhiteSpaceToken(): LinkedToken | null { let curr = this.previous; while (curr != null) { @@ -58,7 +67,7 @@ export class LinkedToken { return null; } - getPreviousOfType(type: TokenType, value?: string): LinkedToken | null { + getPreviousOfType(type: string, value?: string): LinkedToken | null { let curr = this.previous; while (curr != null) { const isType = curr.type === type; @@ -70,7 +79,7 @@ export class LinkedToken { return null; } - getPreviousUntil(type: TokenType, ignoreTypes: TokenType[], value?: string): LinkedToken[] | null { + getPreviousUntil(type: string, ignoreTypes: string[], value?: string): LinkedToken[] | null { let tokens: LinkedToken[] = []; let curr = this.previous; while (curr != null) { @@ -92,7 +101,7 @@ export class LinkedToken { return tokens; } - getNextUntil(type: TokenType, ignoreTypes: TokenType[], value?: string): LinkedToken[] | null { + getNextUntil(type: string, ignoreTypes: string[], value?: string): LinkedToken[] | null { let tokens: LinkedToken[] = []; let curr = this.next; while (curr != null) { @@ -136,7 +145,7 @@ export class LinkedToken { return null; } - getNextOfType(type: TokenType, value?: string): LinkedToken | null { + getNextOfType(type: string, value?: string): LinkedToken | null { let curr = this.next; while (curr != null) { const isType = curr.type === type; diff --git a/public/app/plugins/datasource/cloudwatch/cloudwatch-sql/completion/commands.ts b/public/app/plugins/datasource/cloudwatch/monarch/commands.ts similarity index 100% rename from public/app/plugins/datasource/cloudwatch/cloudwatch-sql/completion/commands.ts rename to public/app/plugins/datasource/cloudwatch/monarch/commands.ts diff --git a/public/app/plugins/datasource/cloudwatch/monarch/linkedTokenBuilder.test.ts b/public/app/plugins/datasource/cloudwatch/monarch/linkedTokenBuilder.test.ts new file mode 100644 index 00000000000..89907a66e85 --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/monarch/linkedTokenBuilder.test.ts @@ -0,0 +1,90 @@ +import { monacoTypes } from '@grafana/ui'; +import MonacoMock from '../__mocks__/monarch/Monaco'; +import TextModel from '../__mocks__/monarch/TextModel'; +import { multiLineFullQuery, singleLineFullQuery } from '../__mocks__/cloudwatch-sql-test-data'; +import { linkedTokenBuilder } from './linkedTokenBuilder'; +import { DESC, SELECT } from '../cloudwatch-sql/language'; +import cloudWatchSqlLanguageDefinition from '../cloudwatch-sql/definition'; +import { SQLTokenTypes } from '../cloudwatch-sql/completion/types'; + +describe('linkedTokenBuilder', () => { + describe('singleLineFullQuery', () => { + const testModel = TextModel(singleLineFullQuery.query); + + it('should add correct references to next LinkedToken', () => { + const position: monacoTypes.IPosition = { lineNumber: 1, column: 0 }; + const current = linkedTokenBuilder( + MonacoMock, + cloudWatchSqlLanguageDefinition, + testModel as monacoTypes.editor.ITextModel, + position, + SQLTokenTypes + ); + + expect(current?.is(SQLTokenTypes.Keyword, SELECT)).toBeTruthy(); + expect(current?.getNextNonWhiteSpaceToken()?.is(SQLTokenTypes.Function, 'AVG')).toBeTruthy(); + }); + + it('should add correct references to previous LinkedToken', () => { + const position: monacoTypes.IPosition = { lineNumber: 1, column: singleLineFullQuery.query.length }; + const current = linkedTokenBuilder( + MonacoMock, + cloudWatchSqlLanguageDefinition, + testModel as monacoTypes.editor.ITextModel, + position, + SQLTokenTypes + ); + expect(current?.is(SQLTokenTypes.Number, '10')).toBeTruthy(); + expect(current?.getPreviousNonWhiteSpaceToken()?.is(SQLTokenTypes.Keyword, 'LIMIT')).toBeTruthy(); + expect( + current?.getPreviousNonWhiteSpaceToken()?.getPreviousNonWhiteSpaceToken()?.is(SQLTokenTypes.Keyword, DESC) + ).toBeTruthy(); + }); + }); + + describe('multiLineFullQuery', () => { + const testModel = TextModel(multiLineFullQuery.query); + + it('should add LinkedToken with whitespace in case empty lines', () => { + const position: monacoTypes.IPosition = { lineNumber: 3, column: 0 }; + const current = linkedTokenBuilder( + MonacoMock, + cloudWatchSqlLanguageDefinition, + testModel as monacoTypes.editor.ITextModel, + position, + SQLTokenTypes + ); + expect(current).not.toBeNull(); + expect(current?.isWhiteSpace()).toBeTruthy(); + }); + + it('should add correct references to next LinkedToken', () => { + const position: monacoTypes.IPosition = { lineNumber: 1, column: 0 }; + const current = linkedTokenBuilder( + MonacoMock, + cloudWatchSqlLanguageDefinition, + testModel as monacoTypes.editor.ITextModel, + position, + SQLTokenTypes + ); + expect(current?.is(SQLTokenTypes.Keyword, SELECT)).toBeTruthy(); + expect(current?.getNextNonWhiteSpaceToken()?.is(SQLTokenTypes.Function, 'AVG')).toBeTruthy(); + }); + + it('should add correct references to previous LinkedToken even when references spans over multiple lines', () => { + const position: monacoTypes.IPosition = { lineNumber: 6, column: 7 }; + const current = linkedTokenBuilder( + MonacoMock, + cloudWatchSqlLanguageDefinition, + testModel as monacoTypes.editor.ITextModel, + position, + SQLTokenTypes + ); + expect(current?.is(SQLTokenTypes.Number, '10')).toBeTruthy(); + expect(current?.getPreviousNonWhiteSpaceToken()?.is(SQLTokenTypes.Keyword, 'LIMIT')).toBeTruthy(); + expect( + current?.getPreviousNonWhiteSpaceToken()?.getPreviousNonWhiteSpaceToken()?.is(SQLTokenTypes.Keyword, DESC) + ).toBeTruthy(); + }); + }); +}); diff --git a/public/app/plugins/datasource/cloudwatch/cloudwatch-sql/completion/linkedTokenBuilder.ts b/public/app/plugins/datasource/cloudwatch/monarch/linkedTokenBuilder.ts similarity index 77% rename from public/app/plugins/datasource/cloudwatch/cloudwatch-sql/completion/linkedTokenBuilder.ts rename to public/app/plugins/datasource/cloudwatch/monarch/linkedTokenBuilder.ts index 24484dd564f..f43002fcb1f 100644 --- a/public/app/plugins/datasource/cloudwatch/cloudwatch-sql/completion/linkedTokenBuilder.ts +++ b/public/app/plugins/datasource/cloudwatch/monarch/linkedTokenBuilder.ts @@ -1,13 +1,15 @@ import type { monacoTypes } from '@grafana/ui'; -import language from '../definition'; import { LinkedToken } from './LinkedToken'; -import { Monaco, TokenType } from './types'; +import { Monaco, TokenTypes } from './types'; +import { LanguageDefinition } from './register'; export function linkedTokenBuilder( monaco: Monaco, + language: LanguageDefinition, model: monacoTypes.editor.ITextModel, - position: monacoTypes.IPosition + position: monacoTypes.IPosition, + tokenTypes: TokenTypes ) { let current: LinkedToken | null = null; let previous: LinkedToken | null = null; @@ -19,7 +21,7 @@ export function linkedTokenBuilder( if (!tokens.length && previous) { const token: monacoTypes.Token = { offset: 0, - type: TokenType.Whitespace, + type: tokenTypes.Whitespace, language: language.id, _tokenBrand: undefined, }; @@ -39,17 +41,18 @@ export function linkedTokenBuilder( }; const value = model.getValueInRange(range); - const sqlToken: LinkedToken = new LinkedToken(token.type, value, range, previous, null); + const newToken: LinkedToken = new LinkedToken(token.type, value, range, previous, null, tokenTypes); if (monaco.Range.containsPosition(range, position)) { - current = sqlToken; + current = newToken; } if (previous) { - previous.next = sqlToken; + previous.next = newToken; } - previous = sqlToken; + previous = newToken; } } + return current; } diff --git a/public/app/plugins/datasource/cloudwatch/monarch/register.ts b/public/app/plugins/datasource/cloudwatch/monarch/register.ts new file mode 100644 index 00000000000..2a69e7156ae --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/monarch/register.ts @@ -0,0 +1,34 @@ +import { Monaco } from '@grafana/ui'; +import { CompletionItemProvider } from './CompletionItemProvider'; +import type * as monacoType from 'monaco-editor/esm/vs/editor/editor.api'; + +export type LanguageDefinition = { + id: string; + extensions: string[]; + aliases: string[]; + mimetypes: string[]; + loader: () => Promise<{ + language: monacoType.languages.IMonarchLanguage; + conf: monacoType.languages.LanguageConfiguration; + }>; +}; + +export const registerLanguage = ( + monaco: Monaco, + language: LanguageDefinition, + completionItemProvider: CompletionItemProvider +) => { + const { id, loader } = language; + + const languages = monaco.languages.getLanguages(); + if (languages.find((l) => l.id === id)) { + return; + } + + monaco.languages.register({ id }); + loader().then((monarch) => { + monaco.languages.setMonarchTokensProvider(id, monarch.language); + monaco.languages.setLanguageConfiguration(id, monarch.conf); + monaco.languages.registerCompletionItemProvider(id, completionItemProvider.getCompletionProvider(monaco, language)); + }); +}; diff --git a/public/app/plugins/datasource/cloudwatch/monarch/types.ts b/public/app/plugins/datasource/cloudwatch/monarch/types.ts new file mode 100644 index 00000000000..5ca960e034e --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/monarch/types.ts @@ -0,0 +1,100 @@ +import { monacoTypes } from '@grafana/ui'; + +export interface TokenTypes { + Parenthesis: string; + Whitespace: string; + Keyword: string; + Delimiter: string; + Operator: string; + Identifier: string; + Type: string; + Function: string; + Number: string; + String: string; + Variable: string; +} + +export enum StatementPosition { + Unknown, + // sql + SelectKeyword, + AfterSelectKeyword, + AfterSelectFuncFirstArgument, + AfterFromKeyword, + SchemaFuncFirstArgument, + SchemaFuncExtraArgument, + FromKeyword, + AfterFrom, + WhereKey, + WhereComparisonOperator, + WhereValue, + AfterWhereValue, + AfterGroupByKeywords, + AfterGroupBy, + AfterOrderByKeywords, + AfterOrderByFunction, + AfterOrderByDirection, + // metric math + PredefinedFunction, + SearchFuncSecondArg, + SearchFuncThirdArg, + PredefinedFuncSecondArg, + AfterFunction, + WithinString, +} + +export enum SuggestionKind { + SelectKeyword, + FunctionsWithArguments, + Metrics, + FromKeyword, + SchemaKeyword, + Namespaces, + LabelKeys, + WhereKeyword, + GroupByKeywords, + OrderByKeywords, + FunctionsWithoutArguments, + LimitKeyword, + SortOrderDirectionKeyword, + ComparisonOperators, + LabelValues, + LogicalOperators, + + // metricmath, + KeywordArguments, + Operators, + Statistic, + Period, +} + +export enum CompletionItemPriority { + High = 'a', + MediumHigh = 'd', + Medium = 'g', + MediumLow = 'k', + Low = 'q', +} + +export interface Editor { + tokenize: (value: string, languageId: string) => monacoTypes.Token[][]; +} + +export interface Range { + containsPosition: (range: monacoTypes.IRange, position: monacoTypes.IPosition) => boolean; + fromPositions: (start: monacoTypes.IPosition, end?: monacoTypes.IPosition) => monacoTypes.Range; +} + +export interface Languages { + CompletionItemInsertTextRule: { + InsertAsSnippet: 4; + }; + CompletionItemKind: { + Function: 1; + }; +} +export interface Monaco { + editor: Editor; + Range: Range; + languages: Languages; +}