CompletionItemProvider.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. import { uniq } from 'lodash';
  2. import { getTemplateSrv, TemplateSrv } from '@grafana/runtime';
  3. import type { Monaco, monacoTypes } from '@grafana/ui';
  4. import { CloudWatchDatasource } from '../../datasource';
  5. import { CompletionItemProvider } from '../../monarch/CompletionItemProvider';
  6. import { LinkedToken } from '../../monarch/LinkedToken';
  7. import { TRIGGER_SUGGEST } from '../../monarch/commands';
  8. import { SuggestionKind, CompletionItemPriority, StatementPosition } from '../../monarch/types';
  9. import {
  10. BY,
  11. FROM,
  12. GROUP,
  13. LIMIT,
  14. ORDER,
  15. SCHEMA,
  16. SELECT,
  17. ASC,
  18. DESC,
  19. WHERE,
  20. COMPARISON_OPERATORS,
  21. LOGICAL_OPERATORS,
  22. STATISTICS,
  23. } from '../language';
  24. import { getStatementPosition } from './statementPosition';
  25. import { getSuggestionKinds } from './suggestionKind';
  26. import { getMetricNameToken, getNamespaceToken } from './tokenUtils';
  27. import { SQLTokenTypes } from './types';
  28. type CompletionItem = monacoTypes.languages.CompletionItem;
  29. export class SQLCompletionItemProvider extends CompletionItemProvider {
  30. region: string;
  31. constructor(datasource: CloudWatchDatasource, templateSrv: TemplateSrv = getTemplateSrv()) {
  32. super(datasource, templateSrv);
  33. this.region = datasource.getActualRegion();
  34. this.getStatementPosition = getStatementPosition;
  35. this.getSuggestionKinds = getSuggestionKinds;
  36. this.tokenTypes = SQLTokenTypes;
  37. }
  38. setRegion(region: string) {
  39. this.region = region;
  40. }
  41. async getSuggestions(
  42. monaco: Monaco,
  43. currentToken: LinkedToken | null,
  44. suggestionKinds: SuggestionKind[],
  45. statementPosition: StatementPosition,
  46. position: monacoTypes.IPosition
  47. ): Promise<CompletionItem[]> {
  48. let suggestions: CompletionItem[] = [];
  49. const invalidRangeToken = currentToken?.isWhiteSpace() || currentToken?.isParenthesis();
  50. const range =
  51. invalidRangeToken || !currentToken?.range ? monaco.Range.fromPositions(position) : currentToken?.range;
  52. const toCompletionItem = (value: string, rest: Partial<CompletionItem> = {}) => {
  53. const item: CompletionItem = {
  54. label: value,
  55. insertText: value,
  56. kind: monaco.languages.CompletionItemKind.Field,
  57. range,
  58. sortText: CompletionItemPriority.Medium,
  59. ...rest,
  60. };
  61. return item;
  62. };
  63. function addSuggestion(value: string, rest: Partial<CompletionItem> = {}) {
  64. suggestions = [...suggestions, toCompletionItem(value, rest)];
  65. }
  66. for (const suggestion of suggestionKinds) {
  67. switch (suggestion) {
  68. case SuggestionKind.SelectKeyword:
  69. addSuggestion(SELECT, {
  70. insertText: `${SELECT} $0`,
  71. insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
  72. kind: monaco.languages.CompletionItemKind.Keyword,
  73. command: TRIGGER_SUGGEST,
  74. });
  75. break;
  76. case SuggestionKind.FunctionsWithArguments:
  77. STATISTICS.map((s) =>
  78. addSuggestion(s, {
  79. insertText: `${s}($0)`,
  80. insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
  81. command: TRIGGER_SUGGEST,
  82. kind: monaco.languages.CompletionItemKind.Function,
  83. })
  84. );
  85. break;
  86. case SuggestionKind.FunctionsWithoutArguments:
  87. STATISTICS.map((s) =>
  88. addSuggestion(s, {
  89. insertText: `${s}() `,
  90. insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
  91. command: TRIGGER_SUGGEST,
  92. kind: monaco.languages.CompletionItemKind.Function,
  93. })
  94. );
  95. break;
  96. case SuggestionKind.Metrics:
  97. {
  98. const namespaceToken = getNamespaceToken(currentToken);
  99. if (namespaceToken?.value) {
  100. // if a namespace is specified, only suggest metrics for the namespace
  101. const metrics = await this.datasource.getMetrics(
  102. this.templateSrv.replace(namespaceToken?.value.replace(/\"/g, '')),
  103. this.templateSrv.replace(this.region)
  104. );
  105. metrics.map((m) => addSuggestion(m.value));
  106. } else {
  107. // If no namespace is specified in the query, just list all metrics
  108. const metrics = await this.datasource.getAllMetrics(this.templateSrv.replace(this.region));
  109. uniq(metrics.map((m) => m.metricName)).map((m) => addSuggestion(m, { insertText: m }));
  110. }
  111. }
  112. break;
  113. case SuggestionKind.FromKeyword:
  114. addSuggestion(FROM, {
  115. insertText: `${FROM} `,
  116. command: TRIGGER_SUGGEST,
  117. });
  118. break;
  119. case SuggestionKind.SchemaKeyword:
  120. addSuggestion(SCHEMA, {
  121. sortText: CompletionItemPriority.High,
  122. insertText: `${SCHEMA}($0)`,
  123. insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
  124. command: TRIGGER_SUGGEST,
  125. kind: monaco.languages.CompletionItemKind.Function,
  126. });
  127. break;
  128. case SuggestionKind.Namespaces:
  129. const metricNameToken = getMetricNameToken(currentToken);
  130. let namespaces = [];
  131. if (metricNameToken?.value) {
  132. // if a metric is specified, only suggest namespaces that actually have that metric
  133. const metrics = await this.datasource.getAllMetrics(this.region);
  134. const metricName = this.templateSrv.replace(metricNameToken.value);
  135. namespaces = metrics.filter((m) => m.metricName === metricName).map((m) => m.namespace);
  136. } else {
  137. // if no metric is specified, just suggest all namespaces
  138. const ns = await this.datasource.getNamespaces();
  139. namespaces = ns.map((n) => n.value);
  140. }
  141. namespaces.map((n) => addSuggestion(`"${n}"`, { insertText: `"${n}"` }));
  142. break;
  143. case SuggestionKind.LabelKeys:
  144. {
  145. const metricNameToken = getMetricNameToken(currentToken);
  146. const namespaceToken = getNamespaceToken(currentToken);
  147. if (namespaceToken?.value) {
  148. let dimensionFilter = {};
  149. let labelKeyTokens;
  150. if (statementPosition === StatementPosition.SchemaFuncExtraArgument) {
  151. labelKeyTokens = namespaceToken?.getNextUntil(this.tokenTypes.Parenthesis, [
  152. this.tokenTypes.Delimiter,
  153. this.tokenTypes.Whitespace,
  154. ]);
  155. } else if (statementPosition === StatementPosition.AfterGroupByKeywords) {
  156. labelKeyTokens = currentToken?.getPreviousUntil(this.tokenTypes.Keyword, [
  157. this.tokenTypes.Delimiter,
  158. this.tokenTypes.Whitespace,
  159. ]);
  160. }
  161. dimensionFilter = (labelKeyTokens || []).reduce((acc, curr) => {
  162. return { ...acc, [curr.value]: null };
  163. }, {});
  164. const keys = await this.datasource.getDimensionKeys(
  165. this.templateSrv.replace(namespaceToken.value.replace(/\"/g, '')),
  166. this.templateSrv.replace(this.region),
  167. dimensionFilter,
  168. metricNameToken?.value ?? ''
  169. );
  170. keys.map((m) => {
  171. const key = /[\s\.-]/.test(m.value) ? `"${m.value}"` : m.value;
  172. addSuggestion(key);
  173. });
  174. }
  175. }
  176. break;
  177. case SuggestionKind.LabelValues:
  178. {
  179. const namespaceToken = getNamespaceToken(currentToken);
  180. const metricNameToken = getMetricNameToken(currentToken);
  181. const labelKey = currentToken?.getPreviousNonWhiteSpaceToken()?.getPreviousNonWhiteSpaceToken();
  182. if (namespaceToken?.value && labelKey?.value && metricNameToken?.value) {
  183. const values = await this.datasource.getDimensionValues(
  184. this.templateSrv.replace(this.region),
  185. this.templateSrv.replace(namespaceToken.value.replace(/\"/g, '')),
  186. this.templateSrv.replace(metricNameToken.value),
  187. this.templateSrv.replace(labelKey.value),
  188. {}
  189. );
  190. values.map((o) =>
  191. addSuggestion(`'${o.value}'`, { insertText: `'${o.value}' `, command: TRIGGER_SUGGEST })
  192. );
  193. }
  194. }
  195. break;
  196. case SuggestionKind.LogicalOperators:
  197. LOGICAL_OPERATORS.map((o) =>
  198. addSuggestion(`${o}`, {
  199. insertText: `${o} `,
  200. command: TRIGGER_SUGGEST,
  201. sortText: CompletionItemPriority.MediumHigh,
  202. })
  203. );
  204. break;
  205. case SuggestionKind.WhereKeyword:
  206. addSuggestion(`${WHERE}`, {
  207. insertText: `${WHERE} `,
  208. command: TRIGGER_SUGGEST,
  209. sortText: CompletionItemPriority.High,
  210. });
  211. break;
  212. case SuggestionKind.ComparisonOperators:
  213. COMPARISON_OPERATORS.map((o) => addSuggestion(`${o}`, { insertText: `${o} `, command: TRIGGER_SUGGEST }));
  214. break;
  215. case SuggestionKind.GroupByKeywords:
  216. addSuggestion(`${GROUP} ${BY}`, {
  217. insertText: `${GROUP} ${BY} `,
  218. command: TRIGGER_SUGGEST,
  219. sortText: CompletionItemPriority.MediumHigh,
  220. });
  221. break;
  222. case SuggestionKind.OrderByKeywords:
  223. addSuggestion(`${ORDER} ${BY}`, {
  224. insertText: `${ORDER} ${BY} `,
  225. command: TRIGGER_SUGGEST,
  226. sortText: CompletionItemPriority.Medium,
  227. });
  228. break;
  229. case SuggestionKind.LimitKeyword:
  230. addSuggestion(LIMIT, { insertText: `${LIMIT} `, sortText: CompletionItemPriority.MediumLow });
  231. break;
  232. case SuggestionKind.SortOrderDirectionKeyword:
  233. [ASC, DESC].map((s) =>
  234. addSuggestion(s, {
  235. insertText: `${s} `,
  236. command: TRIGGER_SUGGEST,
  237. })
  238. );
  239. break;
  240. }
  241. }
  242. // always suggest template variables
  243. this.templateVariables.map((v) => {
  244. addSuggestion(v, {
  245. range,
  246. label: v,
  247. insertText: v,
  248. kind: monaco.languages.CompletionItemKind.Variable,
  249. sortText: CompletionItemPriority.Low,
  250. });
  251. });
  252. return suggestions;
  253. }
  254. }