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
pull/44748/head
Sarah Zinger 3 years ago committed by GitHub
parent b2b584f611
commit 58a71c7e91
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 15
      packages/grafana-ui/src/components/Monaco/theme.ts
  2. 0
      public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-sql-test-data/index.ts
  3. 0
      public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-sql-test-data/multiLineFullQuery.ts
  4. 0
      public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-sql-test-data/multiLineIncompleteQueryWithoutNamespace.ts
  5. 0
      public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-sql-test-data/singleLineEmptyQuery.ts
  6. 0
      public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-sql-test-data/singleLineFullQuery.ts
  7. 0
      public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-sql-test-data/singleLineTwoQueries.ts
  8. 38
      public/app/plugins/datasource/cloudwatch/__mocks__/cloudwatch-sql/Monaco.ts
  9. 28
      public/app/plugins/datasource/cloudwatch/__mocks__/metric-math-test-data/afterFunctionQuery.ts
  10. 6
      public/app/plugins/datasource/cloudwatch/__mocks__/metric-math-test-data/index.ts
  11. 19
      public/app/plugins/datasource/cloudwatch/__mocks__/metric-math-test-data/secondArgAfterSearchQuery.ts
  12. 19
      public/app/plugins/datasource/cloudwatch/__mocks__/metric-math-test-data/secondArgQuery.ts
  13. 10
      public/app/plugins/datasource/cloudwatch/__mocks__/metric-math-test-data/singleLineEmptyQuery.ts
  14. 22
      public/app/plugins/datasource/cloudwatch/__mocks__/metric-math-test-data/thirdArgAfterSearchQuery.ts
  15. 24
      public/app/plugins/datasource/cloudwatch/__mocks__/metric-math-test-data/withinStringQuery.ts
  16. 58
      public/app/plugins/datasource/cloudwatch/__mocks__/monarch/Monaco.ts
  17. 4
      public/app/plugins/datasource/cloudwatch/__mocks__/monarch/TextModel.ts
  18. 63
      public/app/plugins/datasource/cloudwatch/cloudwatch-sql/completion/CompletionItemProvider.ts
  19. 58
      public/app/plugins/datasource/cloudwatch/cloudwatch-sql/completion/linkedTokenBuilder.test.ts
  20. 20
      public/app/plugins/datasource/cloudwatch/cloudwatch-sql/completion/statementPosition.test.ts
  21. 50
      public/app/plugins/datasource/cloudwatch/cloudwatch-sql/completion/statementPosition.ts
  22. 2
      public/app/plugins/datasource/cloudwatch/cloudwatch-sql/completion/suggestionKind.ts
  23. 39
      public/app/plugins/datasource/cloudwatch/cloudwatch-sql/completion/tokenUtils.test.ts
  24. 10
      public/app/plugins/datasource/cloudwatch/cloudwatch-sql/completion/tokenUtils.ts
  25. 91
      public/app/plugins/datasource/cloudwatch/cloudwatch-sql/completion/types.ts
  26. 5
      public/app/plugins/datasource/cloudwatch/cloudwatch-sql/definition.ts
  27. 19
      public/app/plugins/datasource/cloudwatch/cloudwatch-sql/register.ts
  28. 90
      public/app/plugins/datasource/cloudwatch/components/MathExpressionQueryField.tsx
  29. 1
      public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor.tsx
  30. 6
      public/app/plugins/datasource/cloudwatch/components/SQLCodeEditor.tsx
  31. 11
      public/app/plugins/datasource/cloudwatch/datasource.ts
  32. 72
      public/app/plugins/datasource/cloudwatch/metric-math/completion/CompletionItemProvider.test.ts
  33. 123
      public/app/plugins/datasource/cloudwatch/metric-math/completion/CompletionItemProvider.ts
  34. 69
      public/app/plugins/datasource/cloudwatch/metric-math/completion/statementPosition.test.ts
  35. 54
      public/app/plugins/datasource/cloudwatch/metric-math/completion/statementPosition.ts
  36. 18
      public/app/plugins/datasource/cloudwatch/metric-math/completion/suggestionKind.ts
  37. 15
      public/app/plugins/datasource/cloudwatch/metric-math/completion/types.ts
  38. 10
      public/app/plugins/datasource/cloudwatch/metric-math/definition.ts
  39. 156
      public/app/plugins/datasource/cloudwatch/metric-math/language.ts
  40. 91
      public/app/plugins/datasource/cloudwatch/monarch/CompletionItemProvider.ts
  41. 39
      public/app/plugins/datasource/cloudwatch/monarch/LinkedToken.ts
  42. 0
      public/app/plugins/datasource/cloudwatch/monarch/commands.ts
  43. 90
      public/app/plugins/datasource/cloudwatch/monarch/linkedTokenBuilder.test.ts
  44. 19
      public/app/plugins/datasource/cloudwatch/monarch/linkedTokenBuilder.ts
  45. 34
      public/app/plugins/datasource/cloudwatch/monarch/register.ts
  46. 100
      public/app/plugins/datasource/cloudwatch/monarch/types.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') },
],
});
}

@ -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;

@ -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,
},
};

@ -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';

@ -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,
},
};

@ -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,
},
};

@ -0,0 +1,10 @@
import { monacoTypes } from '@grafana/ui';
export const singleLineEmptyQuery = {
query: '',
tokens: [] as monacoTypes.Token[][],
position: {
lineNumber: 1,
column: 1,
},
};

@ -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,
},
};

@ -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,
},
};

@ -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;

@ -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;
},
};
}

@ -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) => {

@ -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();
});
});
});

@ -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);
}

@ -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;
}

@ -1,4 +1,4 @@
import { StatementPosition, SuggestionKind } from './types';
import { StatementPosition, SuggestionKind } from '../../monarch/types';
export function getSuggestionKinds(statementPosition: StatementPosition): SuggestionKind[] {
switch (statementPosition) {

@ -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);

@ -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()) {

@ -1,76 +1,15 @@
import { monacoTypes } from '@grafana/ui';
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;
}
import { TokenTypes } from '../../monarch/types';
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',
};

@ -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;

@ -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));
});
};

@ -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<Props>) {
const onKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter' && (event.shiftKey || event.ctrlKey)) {
event.preventDefault();
onRunQuery();
}
};
export function MathExpressionQueryField({
expression: query,
onChange,
onRunQuery,
datasource,
}: React.PropsWithChildren<Props>) {
const containerRef = useRef<HTMLDivElement>(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 (
<Input
name="Query"
value={query}
placeholder="Enter a math expression"
onBlur={onRunQuery}
onChange={(e) => onChange(e.currentTarget.value)}
onKeyDown={onKeyDown}
/>
<div ref={containerRef}>
<CodeEditor
monacoOptions={{
// without this setting, the auto-resize functionality causes an infinite loop, don't remove it!
scrollBeyondLastLine: false,
// These additional options are style focused and are a subset of those in the query editor in Prometheus
fontSize: 14,
lineNumbers: 'off',
renderLineHighlight: 'none',
scrollbar: {
vertical: 'hidden',
horizontal: 'hidden',
},
suggestFontSize: 12,
wordWrap: 'on',
}}
language={language.id}
value={query}
onBlur={(value) => {
if (value !== query) {
onChange(value);
}
}}
onBeforeEditorMount={(monaco: Monaco) =>
registerLanguage(monaco, language, datasource.metricMathCompletionItemProvider)
}
onEditorDidMount={onEditorMount}
/>
</div>
);
}

@ -102,6 +102,7 @@ export class MetricsQueryEditor extends PureComponent<Props, State> {
onRunQuery={onRunQuery}
expression={query.expression ?? ''}
onChange={(expression) => this.props.onChange({ ...query, expression })}
datasource={datasource}
></MathExpressionQueryField>
)}
</>

@ -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<Props> = ({ region, sql, onChange,
}}
showMiniMap={false}
showLineNumbers={true}
onBeforeEditorMount={(monaco: Monaco) => registerLanguage(monaco, datasource.sqlCompletionItemProvider)}
onBeforeEditorMount={(monaco: Monaco) => registerLanguage(monaco, language, datasource.sqlCompletionItemProvider)}
onEditorDidMount={onEditorMount}
/>
);

@ -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<CloudWatchQuery>): Observable<DataQueryResponse> {

@ -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);
});
});
});

@ -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<CompletionItem[]> {
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<CompletionItem> = {}) => {
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<CompletionItem> = {}) {
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;
}
}

@ -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);
});
});

@ -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;
}

@ -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 [];
}

@ -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',
};

@ -0,0 +1,10 @@
import { LanguageDefinition } from '../monarch/register';
const cloudWatchMetricMathLanguageDefinition: LanguageDefinition = {
id: 'cloudwatch-MetricMath',
extensions: [],
aliases: [],
mimetypes: [],
loader: () => import('./language'),
};
export default cloudWatchMetricMathLanguageDefinition;

@ -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
}

@ -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<CompletionItem[]> {
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,
};
},
};
}
}

@ -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;

@ -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();
});
});
});

@ -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;
}

@ -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));
});
};

@ -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;
}
Loading…
Cancel
Save