diff --git a/package-lock.json b/package-lock.json
index 8fb100e7c..6f0c474f5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -19,7 +19,7 @@
"@gravity-ui/paranoid": "^1.4.1",
"@gravity-ui/react-data-table": "^2.0.1",
"@gravity-ui/uikit": "^6.10.2",
- "@gravity-ui/websql-autocomplete": "^8.0.2",
+ "@gravity-ui/websql-autocomplete": "^8.1.0",
"@reduxjs/toolkit": "^2.2.3",
"axios": "^1.6.8",
"colord": "^2.9.3",
@@ -3969,12 +3969,12 @@
}
},
"node_modules/@gravity-ui/websql-autocomplete": {
- "version": "8.0.2",
- "resolved": "https://registry.npmjs.org/@gravity-ui/websql-autocomplete/-/websql-autocomplete-8.0.2.tgz",
- "integrity": "sha512-Oj0xQU5rSucPxtru0WwUXtnBHq3E6fQJ+Ge11J5qWoFWfALKfJU8xhvfrTWCxwZIC79iqSyhBOHbPvN3kpaX1w==",
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/@gravity-ui/websql-autocomplete/-/websql-autocomplete-8.1.0.tgz",
+ "integrity": "sha512-KHXOrikdszTfOuoEvtBjrsdjrcYAxgi/rfmCkDV8/dNrbvbtUaxvIlFJHka7cRwgZabh0+j7ImjYWS9Pb3fvFA==",
"dependencies": {
- "antlr4-c3": "~3.3.5",
- "antlr4ng": "^2.0.10"
+ "antlr4-c3": "^3.4.1",
+ "antlr4ng": "^3.0.4"
},
"engines": {
"node": ">=16.0"
@@ -6903,25 +6903,25 @@
}
},
"node_modules/antlr4-c3": {
- "version": "3.3.7",
- "resolved": "https://registry.npmjs.org/antlr4-c3/-/antlr4-c3-3.3.7.tgz",
- "integrity": "sha512-F3ndE38wwA6z6AjUbL3heSdEGl4TxulGDPf9xB0/IY4dbRHWBh6XNaqFwur8vHKQk9FS5yNABHeg2wqlqIYO0w==",
+ "version": "3.4.1",
+ "resolved": "https://registry.npmjs.org/antlr4-c3/-/antlr4-c3-3.4.1.tgz",
+ "integrity": "sha512-YLO/ncwUp8w2GNK/lnsYXtMkd8izHCWjxtk7EaTGIZq07THfvI5aHDuhls/RctX3EYDlM9zeTKdqn54eLYNglQ==",
"dependencies": {
- "antlr4ng": "2.0.11"
+ "antlr4ng": "^3.0.1"
}
},
"node_modules/antlr4ng": {
- "version": "2.0.11",
- "resolved": "https://registry.npmjs.org/antlr4ng/-/antlr4ng-2.0.11.tgz",
- "integrity": "sha512-9jM91VVtHSqHkAHQsXHaoaiewFETMvUTI1/tXvwTiFw4f7zke3IGlwEyoKN9NS0FqIwDKFvUNW2e1cKPniTkVQ==",
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/antlr4ng/-/antlr4ng-3.0.4.tgz",
+ "integrity": "sha512-u1Ww6wVv9hq70E9AaYe5qW3ba8hvnjJdO3ZsKnb3iJWFV/medLEEhbyWwXCvvD2ef0ptdaiIUgmaazS/WE6uyQ==",
"peerDependencies": {
- "antlr4ng-cli": "1.0.7"
+ "antlr4ng-cli": "^2.0.0"
}
},
"node_modules/antlr4ng-cli": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/antlr4ng-cli/-/antlr4ng-cli-1.0.7.tgz",
- "integrity": "sha512-qN2FsDBmLvsQcA5CWTrPz8I8gNXeS1fgXBBhI78VyxBSBV/EJgqy8ks6IDTC9jyugpl40csCQ4sL5K4i2YZ/2w==",
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/antlr4ng-cli/-/antlr4ng-cli-2.0.0.tgz",
+ "integrity": "sha512-oAt5OSSYhRQn1PgahtpAP4Vp3BApCoCqlzX7Q8ZUWWls4hX59ryYuu0t7Hwrnfk796OxP/vgIJaqxdltd/oEvQ==",
"peer": true,
"bin": {
"antlr4ng": "index.js"
diff --git a/package.json b/package.json
index 768d68f4d..100b33f72 100644
--- a/package.json
+++ b/package.json
@@ -21,7 +21,7 @@
"@gravity-ui/paranoid": "^1.4.1",
"@gravity-ui/react-data-table": "^2.0.1",
"@gravity-ui/uikit": "^6.10.2",
- "@gravity-ui/websql-autocomplete": "^8.0.2",
+ "@gravity-ui/websql-autocomplete": "^8.1.0",
"@reduxjs/toolkit": "^2.2.3",
"axios": "^1.6.8",
"colord": "^2.9.3",
diff --git a/src/containers/Tenant/Query/QueryEditor/QueryEditor.tsx b/src/containers/Tenant/Query/QueryEditor/QueryEditor.tsx
index f7fb431eb..ec2ff76da 100644
--- a/src/containers/Tenant/Query/QueryEditor/QueryEditor.tsx
+++ b/src/containers/Tenant/Query/QueryEditor/QueryEditor.tsx
@@ -30,6 +30,7 @@ import {
SAVED_QUERIES_KEY,
} from '../../../../utils/constants';
import {useQueryModes, useSetting} from '../../../../utils/hooks';
+import {LANGUAGE_YQL_ID} from '../../../../utils/monaco/yql/constants';
import {QUERY_ACTIONS} from '../../../../utils/query';
import {parseJson} from '../../../../utils/utils';
import type {InitialPaneState} from '../../utils/paneVisibilityToggleHelpers';
@@ -412,7 +413,7 @@ function QueryEditor(props: QueryEditorProps) {
(() => {
const useAutocomplete = Boolean(enableAutocomplete);
return {
quickSuggestions: useAutocomplete,
suggestOnTriggerCharacters: useAutocomplete,
+ acceptSuggestionOnEnter: autocompleteOnEnter ? 'on' : 'off',
...EDITOR_OPTIONS,
};
- }, [enableAutocomplete]);
+ }, [enableAutocomplete, autocompleteOnEnter]);
return options;
}
diff --git a/src/containers/Tenant/Tenant.tsx b/src/containers/Tenant/Tenant.tsx
index e499ba508..ca65f8a1a 100644
--- a/src/containers/Tenant/Tenant.tsx
+++ b/src/containers/Tenant/Tenant.tsx
@@ -80,7 +80,7 @@ function Tenant(props: TenantProps) {
if (tenantName && typeof tenantName === 'string' && previousTenant.current !== tenantName) {
const register = async () => {
const {registerYQLCompletionItemProvider} = await import(
- '../../utils/monaco/yqlSuggestions/registerCompletionItemProvider'
+ '../../utils/monaco/yql/yql.completionItemProvider'
);
registerYQLCompletionItemProvider(tenantName);
};
diff --git a/src/containers/UserSettings/i18n/en.json b/src/containers/UserSettings/i18n/en.json
index cfcb1f4c5..da4c677c0 100644
--- a/src/containers/UserSettings/i18n/en.json
+++ b/src/containers/UserSettings/i18n/en.json
@@ -11,6 +11,9 @@
"settings.editor.autocomplete.title": "Enable autocomplete",
"settings.editor.autocomplete.description": "You’re always able to get suggestions by pressing Ctrl+Space.",
+ "settings.editor.autocomplete-on-enter.title": "Accept suggestion on Enter",
+ "settings.editor.autocomplete-on-enter.description": "Controls whether suggestions should be accepted on Enter, in addition to Tab. Helps to avoid ambiguity between inserting new lines or accepting suggestions.",
+
"settings.theme.title": "Interface theme",
"settings.theme.option-dark": "Dark",
"settings.theme.option-light": "Light",
diff --git a/src/containers/UserSettings/settings.tsx b/src/containers/UserSettings/settings.tsx
index 97c4f25ec..3d0b07967 100644
--- a/src/containers/UserSettings/settings.tsx
+++ b/src/containers/UserSettings/settings.tsx
@@ -2,6 +2,7 @@ import {Flask, PencilToSquare, StarFill} from '@gravity-ui/icons';
import type {IconProps} from '@gravity-ui/uikit';
import {
+ AUTOCOMPLETE_ON_ENTER,
BINARY_DATA_IN_PLAIN_TEXT_DISPLAY,
ENABLE_AUTOCOMPLETE,
INVERTED_DISKS_KEY,
@@ -112,6 +113,12 @@ export const enableAutocompleteSetting: SettingProps = {
description: i18n('settings.editor.autocomplete.description'),
};
+export const autocompleteOnEnterSetting: SettingProps = {
+ settingKey: AUTOCOMPLETE_ON_ENTER,
+ title: i18n('settings.editor.autocomplete-on-enter.title'),
+ description: i18n('settings.editor.autocomplete-on-enter.description'),
+};
+
export const appearanceSection: SettingsSection = {
id: 'appearanceSection',
title: i18n('section.appearance'),
@@ -125,7 +132,7 @@ export const experimentsSection: SettingsSection = {
export const devSettingsSection: SettingsSection = {
id: 'devSettingsSection',
title: i18n('section.dev-setting'),
- settings: [enableAutocompleteSetting],
+ settings: [enableAutocompleteSetting, autocompleteOnEnterSetting],
};
export const generalPage: SettingsPage = {
diff --git a/src/services/api.ts b/src/services/api.ts
index 5d792120b..a96b1875e 100644
--- a/src/services/api.ts
+++ b/src/services/api.ts
@@ -5,6 +5,7 @@ import {backend as BACKEND, metaBackend as META_BACKEND} from '../store';
import type {ComputeApiRequestParams, NodesApiRequestParams} from '../store/reducers/nodes/types';
import type {StorageApiRequestParams} from '../store/reducers/storage/types';
import type {TMetaInfo} from '../types/api/acl';
+import type {TQueryAutocomplete} from '../types/api/autocomplete';
import type {TClusterInfo} from '../types/api/cluster';
import type {TComputeInfo} from '../types/api/compute';
import type {DescribeConsumerResult} from '../types/api/consumer';
@@ -587,6 +588,15 @@ export class YdbEmbeddedAPI extends AxiosWrapper {
whoami() {
return this.get(this.getPath('/viewer/json/whoami'), {});
}
+ autocomplete(params: {database: string; prefix?: string; limit?: number; table?: string[]}) {
+ const {table, ...rest} = params;
+ const tablesParam = table?.join(',');
+ return this.get(
+ this.getPath('/viewer/json/autocomplete'),
+ {...rest, table: tablesParam},
+ {concurrentId: 'sql-autocomplete'},
+ );
+ }
// used if not single cluster mode
getClustersList(_?: never, {signal}: {signal?: AbortSignal} = {}) {
diff --git a/src/services/settings.ts b/src/services/settings.ts
index f6b278c99..f90438653 100644
--- a/src/services/settings.ts
+++ b/src/services/settings.ts
@@ -1,6 +1,7 @@
import {TENANT_PAGES_IDS} from '../store/reducers/tenant/constants';
import {
ASIDE_HEADER_COMPACT_KEY,
+ AUTOCOMPLETE_ON_ENTER,
BINARY_DATA_IN_PLAIN_TEXT_DISPLAY,
ENABLE_AUTOCOMPLETE,
INVERTED_DISKS_KEY,
@@ -38,6 +39,7 @@ export const DEFAULT_USER_SETTINGS: SettingsObject = {
[USE_BACKEND_PARAMS_FOR_TABLES_KEY]: false,
[USE_CLUSTER_BALANCER_AS_BACKEND_KEY]: true,
[ENABLE_AUTOCOMPLETE]: false,
+ [AUTOCOMPLETE_ON_ENTER]: true,
};
class SettingsManager {
diff --git a/src/types/api/autocomplete.ts b/src/types/api/autocomplete.ts
new file mode 100644
index 000000000..f28ccdc60
--- /dev/null
+++ b/src/types/api/autocomplete.ts
@@ -0,0 +1,46 @@
+/**
+ * endpoint: /viewer/json/autocomplete
+ *
+ * source: https://github.com/ydb-platform/ydb/blob/main/ydb/core/viewer/protos/viewer.proto
+ */
+export interface TQueryAutocomplete {
+ Success: boolean;
+ Error?: string[];
+ Result: TAutocompleteResult;
+}
+
+interface TAutocompleteResult {
+ Entities: TAutocompleteEntity[];
+ Total?: number;
+}
+
+export interface TAutocompleteEntity {
+ Name: string;
+ Type: AutocompleteEntityType;
+ Parent: string;
+}
+
+export type AutocompleteEntityType =
+ | 'unknown'
+ | 'dir'
+ | 'table'
+ | 'pers_queue_group'
+ | 'sub_domain'
+ | 'rtmr_volume'
+ | 'block_store_volume'
+ | 'kesus'
+ | 'solomon_volume'
+ | 'table_index'
+ | 'ext_sub_domain'
+ | 'file_store'
+ | 'column_store'
+ | 'column_table'
+ | 'cdc_stream'
+ | 'sequence'
+ | 'replication'
+ | 'blob_depot'
+ | 'external_table'
+ | 'external_data_source'
+ | 'view'
+ | 'column'
+ | 'index';
diff --git a/src/utils/constants.ts b/src/utils/constants.ts
index 68b652bfb..7981b8061 100644
--- a/src/utils/constants.ts
+++ b/src/utils/constants.ts
@@ -134,3 +134,5 @@ export const QUERY_USE_MULTI_SCHEMA_KEY = 'queryUseMultiSchema';
export const USE_CLUSTER_BALANCER_AS_BACKEND_KEY = 'useClusterBalancerAsBacked';
export const ENABLE_AUTOCOMPLETE = 'enableAutocomplete';
+
+export const AUTOCOMPLETE_ON_ENTER = 'autocompleteOnEnter';
diff --git a/src/utils/monaco/index.ts b/src/utils/monaco/index.ts
index 59be381db..540aaab5c 100644
--- a/src/utils/monaco/index.ts
+++ b/src/utils/monaco/index.ts
@@ -1,5 +1,7 @@
import {registerSExpressionLanguage} from './s-expression/registerLanguage';
+import {registerYqlLanguage} from './yql/registerLanguage';
export function registerLanguages() {
registerSExpressionLanguage();
+ registerYqlLanguage();
}
diff --git a/src/utils/monaco/yqlSuggestions/constants.ts b/src/utils/monaco/yql/constants.ts
similarity index 99%
rename from src/utils/monaco/yqlSuggestions/constants.ts
rename to src/utils/monaco/yql/constants.ts
index 0a6abf2e7..8ccfd075e 100644
--- a/src/utils/monaco/yqlSuggestions/constants.ts
+++ b/src/utils/monaco/yql/constants.ts
@@ -1,3 +1,5 @@
+export const LANGUAGE_YQL_ID = 'yql';
+
export const SimpleTypes = [
'String',
'Bool',
diff --git a/src/utils/monaco/yqlSuggestions/generateSuggestions.ts b/src/utils/monaco/yql/generateSuggestions.ts
similarity index 59%
rename from src/utils/monaco/yqlSuggestions/generateSuggestions.ts
rename to src/utils/monaco/yql/generateSuggestions.ts
index d5b746d13..b5eb008f9 100644
--- a/src/utils/monaco/yqlSuggestions/generateSuggestions.ts
+++ b/src/utils/monaco/yql/generateSuggestions.ts
@@ -1,9 +1,12 @@
import type {
ColumnAliasSuggestion,
KeywordSuggestion,
+ YQLEntity,
YqlAutocompleteResult,
} from '@gravity-ui/websql-autocomplete';
-import type * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
+import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
+
+import type {AutocompleteEntityType, TAutocompleteEntity} from '../../../types/api/autocomplete';
import {
AggregateFunctions,
@@ -50,6 +53,33 @@ const CompletionItemKind: {
const re = /[\s'"-/@]/;
+const suggestionEntityToAutocomplete: Partial> = {
+ externalDataSource: ['external_data_source'],
+ replication: ['replication'],
+ table: ['table'],
+ tableStore: ['column_store'],
+ topic: ['pers_queue_group'],
+ view: ['view'],
+ //TODO: add after websql-autocomplete support indexex
+ // index: ['table_index', 'index'],
+};
+
+const commonSuggestionEntities: AutocompleteEntityType[] = ['dir', 'unknown'];
+
+function filterAutocompleteEntities(
+ autocompleteEntities: TAutocompleteEntity[],
+ suggestions: YQLEntity[],
+) {
+ const suggestionsSet = suggestions.reduce((acc, el) => {
+ const autocompleteEntity = suggestionEntityToAutocomplete[el];
+ if (autocompleteEntity) {
+ autocompleteEntity.forEach((el) => acc.add(el));
+ }
+ return acc;
+ }, new Set(commonSuggestionEntities));
+ return autocompleteEntities.filter(({Type}) => suggestionsSet.has(Type));
+}
+
function wrapStringToBackticks(value: string) {
let result = value;
if (value.match(re)) {
@@ -59,11 +89,31 @@ function wrapStringToBackticks(value: string) {
}
function removeBackticks(value: string) {
- let normalizedValue = value;
+ let sliceStart = 0;
+ let sliceEnd = value.length;
if (value.startsWith('`')) {
- normalizedValue = value.slice(1, -1);
+ sliceStart = 1;
+ }
+ if (value.endsWith('`')) {
+ sliceEnd = -1;
+ }
+ return value.slice(sliceStart, sliceEnd);
+}
+
+function removeStartSlash(value: string) {
+ if (value.startsWith('/')) {
+ return value.slice(1);
}
- return normalizedValue;
+ return value;
+}
+
+function normalizeEntityPrefix(value = '', database: string) {
+ let cleanedValue = removeStartSlash(removeBackticks(value));
+ const cleanedDatabase = removeStartSlash(database);
+ if (cleanedValue.startsWith(cleanedDatabase)) {
+ cleanedValue = cleanedValue.slice(cleanedDatabase.length);
+ }
+ return removeStartSlash(cleanedValue);
}
type SuggestionType = keyof Omit;
@@ -83,53 +133,6 @@ const SuggestionsWeight: Record = {
suggestSimpleTypes: 11,
};
-const KEEP_CACHE_MILLIS = 5 * 60 * 1000;
-
-function getColumnsWithCache() {
- const cache = new Map();
- return async (path: string) => {
- const normalizedPath = removeBackticks(path);
- const existed = cache.get(path);
- if (existed) {
- return existed;
- }
- const columns = [];
- const data = await window.api.getDescribe({path: normalizedPath});
- if (data?.Status === 'StatusSuccess') {
- const desc = data.PathDescription;
- if (desc?.Table?.Columns) {
- for (const c of desc.Table.Columns) {
- if (c.Name) {
- columns.push(c.Name);
- }
- }
- }
- if (desc?.ColumnTableDescription?.Schema?.Columns) {
- for (const c of desc.ColumnTableDescription.Schema.Columns) {
- if (c.Name) {
- columns.push(c.Name);
- }
- }
- }
- if (desc?.ExternalTableDescription?.Columns) {
- for (const c of desc.ExternalTableDescription.Columns) {
- if (c.Name) {
- columns.push(c.Name);
- }
- }
- }
- }
-
- cache.set(path, columns);
- setTimeout(() => {
- cache.delete(path);
- }, KEEP_CACHE_MILLIS);
- return columns;
- };
-}
-
-const getColumns = getColumnsWithCache();
-
function getSuggestionIndex(suggestionType: SuggestionType) {
return SuggestionsWeight[suggestionType];
}
@@ -166,21 +169,68 @@ export async function generateColumnsSuggestion(
}
const suggestions: monaco.languages.CompletionItem[] = [];
const multi = suggestColumns.tables.length > 1;
- for (const entity of suggestColumns.tables ?? []) {
- let normalizedEntityName = removeBackticks(entity.name);
- // if it's relative entity path
- if (!normalizedEntityName.startsWith('/')) {
- normalizedEntityName = `${database}/${normalizedEntityName}`;
+
+ const normalizedTableNames =
+ suggestColumns.tables?.map((entity) => {
+ let normalizedEntityName = removeBackticks(entity.name);
+ if (!normalizedEntityName.endsWith('/')) {
+ normalizedEntityName = `${normalizedEntityName}/`;
+ }
+ return normalizeEntityPrefix(normalizedEntityName, database);
+ }) ?? [];
+
+ // remove duplicates if any
+ const filteredTableNames = Array.from(new Set(normalizedTableNames));
+
+ const autocompleteResponse = await window.api.autocomplete({
+ database,
+ table: filteredTableNames,
+ limit: 1000,
+ });
+ if (!autocompleteResponse.Success) {
+ return [];
+ }
+
+ const tableNameToAliasMap = suggestColumns.tables?.reduce(
+ (acc, entity) => {
+ const normalizedEntityName = normalizeEntityPrefix(
+ removeBackticks(entity.name),
+ database,
+ );
+ const aliases = acc[normalizedEntityName] ?? [];
+ if (entity.alias) {
+ aliases.push(entity.alias);
+ }
+ acc[normalizedEntityName] = aliases;
+ return acc;
+ },
+ {} as Record,
+ );
+
+ autocompleteResponse.Result.Entities.forEach((col) => {
+ if (col.Type !== 'column') {
+ return;
}
- const fields = await getColumns(normalizedEntityName);
- fields.forEach((columnName: string) => {
- const normalizedName = wrapStringToBackticks(columnName);
+ const normalizedName = wrapStringToBackticks(col.Name);
+
+ const normalizedParentName = normalizeEntityPrefix(col.Parent, database);
+ const aliases = tableNameToAliasMap[normalizedParentName];
+ if (aliases?.length) {
+ aliases.forEach((a) => {
+ const columnNameSuggestion = `${a}.${normalizedName}`;
+ suggestions.push({
+ label: columnNameSuggestion,
+ insertText: columnNameSuggestion,
+ kind: CompletionItemKind.Field,
+ detail: 'Column',
+ range: rangeToInsertSuggestion,
+ sortText: suggestionIndexToWeight(getSuggestionIndex('suggestColumns')),
+ });
+ });
+ } else {
let columnNameSuggestion = normalizedName;
- if (entity.alias) {
- columnNameSuggestion = `${entity.alias}.${normalizedName}`;
- } else if (multi) {
- // no need to wrap entity.name to backticks, because it's already with them if needed
- columnNameSuggestion = `${entity.name}.${normalizedName}`;
+ if (multi) {
+ columnNameSuggestion = `${wrapStringToBackticks(normalizedParentName)}.${normalizedName}`;
}
suggestions.push({
label: columnNameSuggestion,
@@ -190,8 +240,8 @@ export async function generateColumnsSuggestion(
range: rangeToInsertSuggestion,
sortText: suggestionIndexToWeight(getSuggestionIndex('suggestColumns')),
});
- });
- }
+ }
+ });
return suggestions;
}
@@ -229,8 +279,40 @@ export function generateKeywordsSuggestion(
}
export async function generateEntitiesSuggestion(
- _rangeToInsertSuggestion: monaco.IRange,
+ rangeToInsertSuggestion: monaco.IRange,
+ suggestEntities: YQLEntity[],
+ database: string,
+ prefix?: string,
): Promise {
+ const normalizedPrefix = normalizeEntityPrefix(prefix, database);
+ const data = await window.api.autocomplete({database, prefix: normalizedPrefix, limit: 1000});
+ const withBackticks = prefix?.startsWith('`');
+ if (data.Success) {
+ const filteredEntities = filterAutocompleteEntities(data.Result.Entities, suggestEntities);
+ return filteredEntities.reduce((acc, {Name, Type}) => {
+ const isDir = Type === 'dir';
+ const label = isDir ? `${Name}/` : Name;
+ let labelAsSnippet;
+ if (isDir && !withBackticks) {
+ labelAsSnippet = `\`${label}$0\``;
+ }
+ acc.push({
+ label,
+ insertText: labelAsSnippet ?? label,
+ kind: isDir ? CompletionItemKind.Folder : CompletionItemKind.Text,
+ insertTextRules: labelAsSnippet
+ ? monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet
+ : monaco.languages.CompletionItemInsertTextRule.None,
+ detail: Type,
+ range: rangeToInsertSuggestion,
+ command: label.endsWith('/')
+ ? {id: 'editor.action.triggerSuggest', title: ''}
+ : undefined,
+ sortText: suggestionIndexToWeight(getSuggestionIndex('suggestEntity')),
+ });
+ return acc;
+ }, [] as monaco.languages.CompletionItem[]);
+ }
return [];
}
export async function generateSimpleFunctionsSuggestion(
diff --git a/src/utils/monaco/yql/registerLanguage.ts b/src/utils/monaco/yql/registerLanguage.ts
new file mode 100644
index 000000000..ed2ef8fcd
--- /dev/null
+++ b/src/utils/monaco/yql/registerLanguage.ts
@@ -0,0 +1,10 @@
+import * as monaco from 'monaco-editor';
+
+import {LANGUAGE_YQL_ID} from './constants';
+import {conf, getLanguage} from './yql';
+
+export function registerYqlLanguage() {
+ monaco.languages.register({id: LANGUAGE_YQL_ID});
+ monaco.languages.setMonarchTokensProvider(LANGUAGE_YQL_ID, getLanguage());
+ monaco.languages.setLanguageConfiguration(LANGUAGE_YQL_ID, conf);
+}
diff --git a/src/utils/monaco/yqlSuggestions/registerCompletionItemProvider.ts b/src/utils/monaco/yql/yql.completionItemProvider.ts
similarity index 79%
rename from src/utils/monaco/yqlSuggestions/registerCompletionItemProvider.ts
rename to src/utils/monaco/yql/yql.completionItemProvider.ts
index 24bb74b6e..a65446341 100644
--- a/src/utils/monaco/yqlSuggestions/registerCompletionItemProvider.ts
+++ b/src/utils/monaco/yql/yql.completionItemProvider.ts
@@ -1,5 +1,6 @@
import * as monaco from 'monaco-editor';
+import {LANGUAGE_YQL_ID} from './constants';
import {createProvideSuggestionsFunction} from './yqlSuggestions';
let completionProvider: monaco.IDisposable | undefined;
@@ -12,8 +13,8 @@ function disableCodeSuggestions(): void {
export function registerYQLCompletionItemProvider(database: string) {
disableCodeSuggestions();
- completionProvider = monaco.languages.registerCompletionItemProvider('sql', {
- triggerCharacters: [' ', '\n', '', ',', '.', '`', '('],
+ completionProvider = monaco.languages.registerCompletionItemProvider(LANGUAGE_YQL_ID, {
+ triggerCharacters: [' ', '', ',', '.', '`', '(', '/'],
provideCompletionItems: createProvideSuggestionsFunction(database),
});
}
diff --git a/src/utils/monaco/yql/yql.keywords.ts b/src/utils/monaco/yql/yql.keywords.ts
new file mode 100644
index 000000000..4596981c0
--- /dev/null
+++ b/src/utils/monaco/yql/yql.keywords.ts
@@ -0,0 +1,13 @@
+export const keywords =
+ '$row|$rows|action|all|and|any|as|asc|assume|begin|bernoulli|between|by|case|columns|commit|compact|create|cross|cube|declare|define|delete|desc|dict|discard|distinct|do|drop|else|empty_action|end|erase|evaluate|exclusion|exists|export|flatten|for|from|full|group|grouping|having|if|ignore|ilike|import|in|inner|insert|into|is|join|left|like|limit|list|match|not|null|nulls|offset|on|only|optional|or|order|over|partition|pragma|presort|process|reduce|regexp|repeatable|replace|respect|result|return|right|rlike|rollup|sample|schema|select|semi|set|sets|stream|subquery|table|tablesample|then|truncate|union|update|upsert|use|using|values|view|when|where|window|with|without|xor'.split(
+ '|',
+ );
+export const typeKeywords =
+ 'bool|date|datetime|decimal|double|float|int16|int32|int64|int8|interval|json|string|timestamp|tzdate|tzdatetime|tztimestamp|uint16|uint32|uint64|uint8|utf8|uuid|yson'.split(
+ '|',
+ );
+
+export const builtinFunctions =
+ 'abs|aggregate_by|aggregate_list|aggregate_list_distinct|agg_list|agg_list_distinct|as_table|avg|avg_if|adaptivedistancehistogram|adaptivewardhistogram|adaptiveweighthistogram|addmember|addtimezone|aggregateflatten|aggregatetransforminput|aggregatetransformoutput|aggregationfactory|asatom|asdict|asdictstrict|asenum|aslist|asliststrict|asset|assetstrict|asstruct|astagged|astuple|asvariant|atomcode|bitcast|bit_and|bit_or|bit_xor|bool_and|bool_or|bool_xor|bottom|bottom_by|blockwardhistogram|blockweighthistogram|cast|coalesce|concat|concat_strict|correlation|count|count_if|covariance|covariance_population|covariance_sample|callableargument|callableargumenttype|callableresulttype|callabletype|callabletypecomponents|callabletypehandle|choosemembers|combinemembers|countdistinctestimate|currentauthenticateduser|currentoperationid|currentoperationsharedid|currenttzdate|currenttzdatetime|currenttztimestamp|currentutcdate|currentutcdatetime|currentutctimestamp|dense_rank|datatype|datatypecomponents|datatypehandle|dictaggregate|dictcontains|dictcreate|dicthasitems|dictitems|dictkeytype|dictkeys|dictlength|dictlookup|dictpayloadtype|dictpayloads|dicttype|dicttypecomponents|dicttypehandle|each|each_strict|emptydicttype|emptydicttypehandle|emptylisttype|emptylisttypehandle|endswith|ensure|ensureconvertibleto|ensuretype|enum|evaluateatom|evaluatecode|evaluateexpr|evaluatetype|expandstruct|filter|filter_strict|find|first_value|folder|filecontent|filepath|flattenmembers|forceremovemember|forceremovemembers|forcerenamemembers|forcespreadmembers|formatcode|formattype|frombytes|funccode|greatest|grouping|gathermembers|generictype|histogram|hll|hyperloglog|if|if_strict|instanceof|json_exists|json_query|json_value|jointablerow|just|lag|last_value|lead|least|len|length|like|likely|like_strict|lambdaargumentscount|lambdacode|linearhistogram|listaggregate|listall|listany|listavg|listcode|listcollect|listconcat|listcreate|listdistinct|listenumerate|listextend|listextendstrict|listextract|listfilter|listflatmap|listflatten|listfromrange|listhas|listhasitems|listhead|listindexof|listitemtype|listlast|listlength|listmap|listmax|listmin|listnotnull|listreplicate|listreverse|listskip|listskipwhile|listskipwhileinclusive|listsort|listsortasc|listsortdesc|listsum|listtake|listtakewhile|listtakewhileinclusive|listtype|listtypehandle|listunionall|listuniq|listzip|listzipall|loghistogram|logarithmichistogram|max|max_by|max_of|median|min|min_by|min_of|mode|multi_aggregate_by|nanvl|nvl|nothing|nulltype|nulltypehandle|optionalitemtype|optionaltype|optionaltypehandle|percentile|parsefile|parsetype|parsetypehandle|pickle|quotecode|range|range_strict|rank|regexp|regexp_strict|rfind|row_number|random|randomnumber|randomuuid|removemember|removemembers|removetimezone|renamemembers|replacemember|reprcode|resourcetype|resourcetypehandle|resourcetypetag|some|stddev|stddev_population|stddev_sample|substring|sum|sum_if|sessionstart|sessionwindow|setcreate|setdifference|setincludes|setintersection|setisdisjoint|setsymmetricdifference|setunion|spreadmembers|stablepickle|startswith|staticmap|streamitemtype|streamtype|streamtypehandle|structmembertype|structmembers|structtypecomponents|structtypehandle|subqueryextend|subqueryextendfor|subquerymerge|subquerymergefor|subqueryunionall|subqueryunionallfor|subqueryunionmerge|subqueryunionmergefor|top|topfreq|top_by|tablename|tablepath|tablerecordindex|tablerow|taggedtype|taggedtypecomponents|taggedtypehandle|tobytes|todict|tomultidict|toset|tosorteddict|tosortedmultidict|trymember|tupleelementtype|tupletype|tupletypecomponents|tupletypehandle|typehandle|typekind|typeof|udaf|unittype|unpickle|untag|unwrap|variance|variance_population|variance_sample|variant|varianttype|varianttypehandle|variantunderlyingtype|voidtype|voidtypehandle|way|worldcode'.split(
+ '|',
+ );
diff --git a/src/utils/monaco/yql/yql.ts b/src/utils/monaco/yql/yql.ts
new file mode 100644
index 000000000..1f82997f9
--- /dev/null
+++ b/src/utils/monaco/yql/yql.ts
@@ -0,0 +1,197 @@
+import type * as monaco from 'monaco-editor';
+
+import {builtinFunctions, keywords, typeKeywords} from './yql.keywords';
+
+export const conf: monaco.languages.LanguageConfiguration = {
+ comments: {
+ lineComment: '--',
+ blockComment: ['/*', '*/'],
+ },
+ brackets: [
+ ['{', '}'],
+ ['[', ']'],
+ ['(', ')'],
+ ],
+ autoClosingPairs: [
+ {open: '{', close: '}'},
+ {open: '[', close: ']'},
+ {open: '(', close: ')'},
+ {open: '"', close: '"'},
+ {open: "'", close: "'"},
+ {open: '`', close: '`'},
+ ],
+ surroundingPairs: [
+ {open: '{', close: '}'},
+ {open: '[', close: ']'},
+ {open: '(', close: ')'},
+ {open: '"', close: '"'},
+ {open: "'", close: "'"},
+ {open: '`', close: '`'},
+ ],
+ wordPattern: /(-?\d*\.\d\w*)|([^`~!@#%^&*()\-=+[{\]}\\|;:'",./?\s]+)/g,
+};
+
+interface LanguageOptions {
+ ansi?: boolean;
+}
+export function getLanguage({
+ ansi = false,
+}: LanguageOptions = {}): monaco.languages.IMonarchLanguage & Record {
+ return {
+ defaultToken: 'text',
+ ignoreCase: true,
+
+ brackets: [
+ {open: '[', close: ']', token: 'delimiter.square'},
+ {open: '(', close: ')', token: 'delimiter.parenthesis'},
+ {open: '{', close: '}', token: 'delimiter.curly'},
+ ],
+
+ keywords,
+
+ typeKeywords,
+
+ constants: ['true', 'false'],
+
+ builtinFunctions,
+
+ operators: [
+ '+',
+ '-',
+ '/',
+ '//',
+ '%',
+ '<@>',
+ '@>',
+ '<@',
+ '&',
+ '^',
+ '~',
+ '<',
+ '>',
+ '<=',
+ '>=',
+ '=>',
+ '==',
+ '!=',
+ '<>',
+ '=',
+ ],
+
+ symbols: /[=> {
- const cursorPosition: CursorPosition = {
- line: monacoCursorPosition.lineNumber,
- column: monacoCursorPosition.column,
- };
const rangeToInsertSuggestion = getRangeToInsertSuggestion(model, monacoCursorPosition);
const suggestions = await getSuggestions(
model,
- cursorPosition,
+ monacoCursorPosition,
rangeToInsertSuggestion,
database,
);
@@ -39,14 +35,32 @@ export function createProvideSuggestionsFunction(database: string) {
};
}
+function getEntityNameAtCursor(model: Monaco.editor.ITextModel, cursorPosition: Monaco.Position) {
+ const prevWord = model.findPreviousMatch(
+ '\\s(`?[^\\s]*)',
+ cursorPosition,
+ true,
+ false,
+ null,
+ true,
+ );
+ const nextWord = model.findNextMatch('([^\\s]*)`?', cursorPosition, true, false, null, true);
+
+ return `${prevWord?.matches?.[1] ?? ''}${nextWord?.matches?.[1] ?? ''}`;
+}
+
async function getSuggestions(
model: Monaco.editor.ITextModel,
- cursorPosition: CursorPosition,
+ cursorPosition: Monaco.Position,
rangeToInsertSuggestion: Monaco.IRange,
database: string,
): Promise {
const {parseYqlQuery} = await import('@gravity-ui/websql-autocomplete');
- const parseResult = parseYqlQuery(model.getValue(), cursorPosition);
+ const cursorForParsing: CursorPosition = {
+ line: cursorPosition.lineNumber,
+ column: cursorPosition.column,
+ };
+ const parseResult = parseYqlQuery(model.getValue(), cursorForParsing);
let entitiesSuggestions: Monaco.languages.CompletionItem[] = [];
let functionsSuggestions: Monaco.languages.CompletionItem[] = [];
let aggregateFunctionsSuggestions: Monaco.languages.CompletionItem[] = [];
@@ -57,7 +71,14 @@ async function getSuggestions(
let pragmasSuggestions: Monaco.languages.CompletionItem[] = [];
if (parseResult.suggestEntity) {
- entitiesSuggestions = await generateEntitiesSuggestion(rangeToInsertSuggestion);
+ const entityNamePrefix = getEntityNameAtCursor(model, cursorPosition);
+
+ entitiesSuggestions = await generateEntitiesSuggestion(
+ rangeToInsertSuggestion,
+ parseResult.suggestEntity,
+ database,
+ entityNamePrefix,
+ );
}
if (parseResult.suggestFunctions) {
functionsSuggestions = await generateSimpleFunctionsSuggestion(rangeToInsertSuggestion);