From 0f29fa64008185a0ea10437ddb9557e3658000f4 Mon Sep 17 00:00:00 2001 From: jialan Date: Mon, 4 Nov 2024 19:33:53 +0800 Subject: [PATCH] feat: support built-in sql snippets --- src/baseSQLWorker.ts | 22 +++- src/languageFeatures.ts | 15 ++- src/languages/flink/flink.snippet.ts | 14 +++ src/languages/hive/hive.snippet.ts | 117 ++++++++++++++++++ src/languages/impala/impala.snippet.ts | 14 +++ src/languages/mysql/mysql.snippet.ts | 14 +++ src/languages/pgsql/pgsql.snippet.ts | 14 +++ src/languages/spark/spark.snippet.ts | 14 +++ src/languages/trino/trino.snippet.ts | 14 +++ src/main.ts | 1 + src/monaco.contribution.ts | 37 +++++- src/setupLanguageFeatures.ts | 32 ++++- src/snippets.ts | 7 ++ .../languages/helpers/completionService.ts | 18 ++- website/src/languages/index.ts | 1 - 15 files changed, 321 insertions(+), 13 deletions(-) create mode 100644 src/languages/flink/flink.snippet.ts create mode 100644 src/languages/hive/hive.snippet.ts create mode 100644 src/languages/impala/impala.snippet.ts create mode 100644 src/languages/mysql/mysql.snippet.ts create mode 100644 src/languages/pgsql/pgsql.snippet.ts create mode 100644 src/languages/spark/spark.snippet.ts create mode 100644 src/languages/trino/trino.snippet.ts create mode 100644 src/snippets.ts diff --git a/src/baseSQLWorker.ts b/src/baseSQLWorker.ts index 3d3029cf..4fe0166c 100644 --- a/src/baseSQLWorker.ts +++ b/src/baseSQLWorker.ts @@ -2,6 +2,7 @@ import { BasicSQL } from 'dt-sql-parser/dist/parser/common/basicSQL'; import { worker } from './fillers/monaco-editor-core'; import { Suggestions, ParseError, EntityContext } from 'dt-sql-parser'; import { Position } from './fillers/monaco-editor-core'; +import { SemanticContext } from 'dt-sql-parser/dist/parser/common/types'; export interface ICreateData { languageId: string; @@ -45,7 +46,11 @@ export abstract class BaseSQLWorker { async doCompletionWithEntities( code: string, position: Position - ): Promise<[Suggestions | null, EntityContext[] | null]> { + ): Promise<{ + suggestions: Suggestions | null; + allEntities: EntityContext[] | null; + context: SemanticContext | null; + }> { code = code || this.getTextDocument(); if (code) { const suggestions = this.parser.getSuggestionAtCaretPosition(code, position); @@ -53,9 +58,20 @@ export abstract class BaseSQLWorker { if (suggestions?.syntax?.length) { allEntities = this.parser.getAllEntities(code, position); } - return Promise.resolve([suggestions, allEntities]); + const semanticContext = this.parser.getSemanticContextAtCaretPosition(code, position); + + return Promise.resolve({ + suggestions, + allEntities, + context: semanticContext + }); } - return Promise.resolve([null, null]); + + return Promise.resolve({ + suggestions: null, + allEntities: null, + context: null + }); } async getAllEntities(code: string, position?: Position): Promise { diff --git a/src/languageFeatures.ts b/src/languageFeatures.ts index 2a9a869e..eb3aa0aa 100644 --- a/src/languageFeatures.ts +++ b/src/languageFeatures.ts @@ -11,7 +11,7 @@ import { import { debounce } from './common/utils'; import { BaseSQLWorker } from './baseSQLWorker'; import type { ParseError } from 'dt-sql-parser'; -import type { LanguageServiceDefaults } from './monaco.contribution'; +import type { CompletionSnippet, LanguageServiceDefaults } from './monaco.contribution'; export interface WorkerAccessor { (...uris: Uri[]): Promise; @@ -159,13 +159,22 @@ export class CompletionAdapter } return worker.doCompletionWithEntities(code, position); }) - .then(([suggestions, allEntities]) => { + .then(async ({ suggestions, allEntities, context: semanticContext }) => { + let snippets: CompletionSnippet[] = []; + if (semanticContext?.isNewStatement) { + snippets = this._defaults.completionSnippets.map((item) => ({ + ...item, + insertText: typeof item.body === 'string' ? item.body : item.body.join('\n') + })); + } + return this._defaults.completionService( model, position, context, suggestions, - allEntities + allEntities, + snippets ); }) .then((completions) => { diff --git a/src/languages/flink/flink.snippet.ts b/src/languages/flink/flink.snippet.ts new file mode 100644 index 00000000..2b4bb55d --- /dev/null +++ b/src/languages/flink/flink.snippet.ts @@ -0,0 +1,14 @@ +import type { CompletionSnippetOption } from 'src/monaco.contribution'; + +export const flinkSnippets: CompletionSnippetOption[] = [ + { + prefix: 'INSERT', + label: 'INSERT代码片段', + body: ['insert', 'into', '\t`${1:table1}`', 'values', '\t(`$2`);', '${3}'] + }, + { + prefix: 'SELECT', + label: 'SELECT 通用模板', + body: ['select', '\t${1:id}', 'from', '\t${2:table1};', '${3}'] + } +]; diff --git a/src/languages/hive/hive.snippet.ts b/src/languages/hive/hive.snippet.ts new file mode 100644 index 00000000..9c593dfb --- /dev/null +++ b/src/languages/hive/hive.snippet.ts @@ -0,0 +1,117 @@ +import type { CompletionSnippetOption } from 'src/monaco.contribution'; + +export const hiveSnippets: CompletionSnippetOption[] = [ + { + label: 'select', + prefix: 'SELECT', + body: ['SELECT ${2:column1}, ${3:column2} FROM ${1:table_name};\n$4'] + }, + { + label: 'select join', + prefix: 'SELECT-JOIN', + body: [ + 'SELECT ${8:column1} FROM ${1:table_name} ${2:t1}', + '${3:LEFT} JOIN ${4:table2} ${5:t2} ON ${2:t1}.${6:column1} = ${5:t2}.${7:column2};\n$9' + ] + }, + { + label: 'select order-by', + prefix: 'SELECT-ORDERBY', + body: [ + 'SELECT ${2:column1}, ${3:column2} FROM ${1:table_name} ORDER BY ${4:column1} ${5:desc};\n$6' + ] + }, + { + label: 'insert', + prefix: 'INSERT', + body: [ + 'INSERT INTO ${1:table_name} (${2:column1}, ${3:column2}) VALUES (${4:value1}, ${5:value2});\n$6' + ] + }, + { + label: 'insert into select', + prefix: 'INSERT', + body: [ + 'INSERT INTO TABLE ${1:table_name}', + 'SELECT ${2:column1}, ${3:column3}', + 'FROM ${4:source_table}', + 'WHERE ${5:conditions};\n$6' + ] + }, + { + label: 'update', + prefix: 'UPDATE', + body: [ + 'UPDATE ${1:table_name} SET ${2:column1} = ${3:value1} WHERE ${4:column2} = ${5:value2};\n$6' + ] + }, + { + label: 'delete', + prefix: 'DELETE', + body: ['DELETE FROM ${1:table_name} WHERE ${2:column1} = ${3:value1};\n$4'] + }, + { + label: 'create table', + prefix: 'CREATE-TABLE', + body: [ + 'CREATE TABLE IF NOT EXISTS ${1:table_name} (', + '\t${2:column1} ${3:STRING},', + '\t${4:column2} ${5:STRING}', + ')', + "COMMENT '${6:table_alias}'", + 'ROW FORMAT ${7:DELIMITE}', + "FIELDS TERMINATED BY '${8:\\t}'", + 'STORED AS ${9:TEXTFILE};\n$10' + ] + }, + { + label: 'create table as select', + prefix: 'CREATE-TABLE-AS-SELECT', + body: [ + 'CREATE TABLE ${1:table_name} AS', + 'SELECT ${2:column1}, ${3:column2}', + 'FROM ${4:source_table}', + 'WHERE ${5:conditions};\n$6' + ] + }, + { + label: 'create partition table', + prefix: 'CREATE-PARTITION-TABLE', + body: [ + 'CREATE TABLE IF NOT EXISTS ${1:table_name} (', + '\t${2:column1} ${3:STRING},', + '\t${4:column2} ${5:STRING}', + ')', + "COMMENT '${6:table_alias}'", + 'PARTITIONED BY (${7:part_column_name} STRING)', + 'ROW FORMAT ${8:DELIMITED}', + "FIELDS TERMINATED BY '${9:\\t}'", + 'STORED AS ${10:TEXTFILE};\n$11' + ] + }, + { + label: 'create bucket table', + prefix: 'CREATE-BUCKET-TABLE', + body: [ + 'CREATE TABLE IF NOT EXISTS ${1:table_name} (', + '\t${2:column1} ${3:STRING},', + '\t${4:column2} ${5:STRING}', + ')', + "COMMENT '${6:table_alias}'", + 'PARTITIONED BY (${7:part_column_name} STRING)', + 'CLUSTERED BY (${8:bucket_column_name})', + 'INTO ${9:1} BUCKETS', + 'ROW FORMAT ${10:DELIMITED}', + "FIELDS TERMINATED BY '${11:\\t}'", + 'STORED AS ${12:TEXTFILE};\n$13' + ] + }, + { + label: 'alter table partition', + prefix: 'ALTER-TABLE-PARTITION', + body: [ + 'ALTER TABLE ${1:table_name}', + "${2:ADD} PARTITION (${3:partition_column}='${4:partition_value}');\n$5" + ] + } +]; diff --git a/src/languages/impala/impala.snippet.ts b/src/languages/impala/impala.snippet.ts new file mode 100644 index 00000000..c2b0ad00 --- /dev/null +++ b/src/languages/impala/impala.snippet.ts @@ -0,0 +1,14 @@ +import type { CompletionSnippetOption } from 'src/monaco.contribution'; + +export const impalaSnippets: CompletionSnippetOption[] = [ + { + prefix: 'INSERT', + label: 'INSERT代码片段', + body: ['insert', 'into', '\t`${1:table1}`', 'values', '\t(`$2`);', '${3}'] + }, + { + prefix: 'SELECT', + label: 'SELECT 通用模板', + body: ['select', '\t${1:id}', 'from', '\t${2:table1};', '${3}'] + } +]; diff --git a/src/languages/mysql/mysql.snippet.ts b/src/languages/mysql/mysql.snippet.ts new file mode 100644 index 00000000..a68d167b --- /dev/null +++ b/src/languages/mysql/mysql.snippet.ts @@ -0,0 +1,14 @@ +import type { CompletionSnippetOption } from 'src/monaco.contribution'; + +export const mysqlSnippets: CompletionSnippetOption[] = [ + { + prefix: 'INSERT', + label: 'INSERT代码片段', + body: ['insert', 'into', '\t`${1:table1}`', 'values', '\t(`$2`);', '${3}'] + }, + { + prefix: 'SELECT', + label: 'SELECT 通用模板', + body: ['select', '\t${1:id}', 'from', '\t${2:table1};', '${3}'] + } +]; diff --git a/src/languages/pgsql/pgsql.snippet.ts b/src/languages/pgsql/pgsql.snippet.ts new file mode 100644 index 00000000..4affe836 --- /dev/null +++ b/src/languages/pgsql/pgsql.snippet.ts @@ -0,0 +1,14 @@ +import type { CompletionSnippetOption } from 'src/monaco.contribution'; + +export const pgsqlSnippets: CompletionSnippetOption[] = [ + { + prefix: 'INSERT', + label: 'INSERT代码片段', + body: ['insert', 'into', '\t`${1:table1}`', 'values', '\t(`$2`);', '${3}'] + }, + { + prefix: 'SELECT', + label: 'SELECT 通用模板', + body: ['select', '\t${1:id}', 'from', '\t${2:table1};', '${3}'] + } +]; diff --git a/src/languages/spark/spark.snippet.ts b/src/languages/spark/spark.snippet.ts new file mode 100644 index 00000000..ffaf5904 --- /dev/null +++ b/src/languages/spark/spark.snippet.ts @@ -0,0 +1,14 @@ +import type { CompletionSnippetOption } from 'src/monaco.contribution'; + +export const sparkSnippets: CompletionSnippetOption[] = [ + { + prefix: 'INSERT', + label: 'INSERT代码片段', + body: ['insert', 'into', '\t`${1:table1}`', 'values', '\t(`$2`);', '${3}'] + }, + { + prefix: 'SELECT', + label: 'SELECT 通用模板', + body: ['select', '\t${1:id}', 'from', '\t${2:table1};', '${3}'] + } +]; diff --git a/src/languages/trino/trino.snippet.ts b/src/languages/trino/trino.snippet.ts new file mode 100644 index 00000000..633a3e23 --- /dev/null +++ b/src/languages/trino/trino.snippet.ts @@ -0,0 +1,14 @@ +import type { CompletionSnippetOption } from 'src/monaco.contribution'; + +export const trinoSnippets: CompletionSnippetOption[] = [ + { + prefix: 'INSERT', + label: 'INSERT代码片段', + body: ['insert', 'into', '\t`${1:table1}`', 'values', '\t(`$2`);', '${3}'] + }, + { + prefix: 'SELECT', + label: 'SELECT 通用模板', + body: ['select', '\t${1:id}', 'from', '\t${2:table1};', '${3}'] + } +]; diff --git a/src/main.ts b/src/main.ts index f743ce30..41630517 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,6 +4,7 @@ export * from './languageService'; export * from './setupLanguageFeatures'; export * from './common/constants'; export * from './theme'; +export * as snippets from './snippets'; export { EntityContextType, StmtContextType } from 'dt-sql-parser'; diff --git a/src/monaco.contribution.ts b/src/monaco.contribution.ts index e655fc54..ed54f0ed 100644 --- a/src/monaco.contribution.ts +++ b/src/monaco.contribution.ts @@ -31,7 +31,8 @@ export type CompletionService = ( position: Position, completionContext: languages.CompletionContext, suggestions: Suggestions | null, - entities: EntityContext[] | null + entities: EntityContext[] | null, + snippets?: CompletionSnippet[] ) => Promise; export interface CompletionOptions { @@ -42,8 +43,20 @@ export interface CompletionOptions { */ completionService: CompletionService; triggerCharacters: string[]; + snippets: CompletionSnippetOption[]; } +export interface CompletionSnippet { + prefix: string; + label: string; + body: string | string[]; + // generated by body + insertText?: string; + description?: string; +} + +export type CompletionSnippetOption = Omit; + export interface ModeConfiguration { /** * Defines whether the built-in completionItemProvider is enabled. @@ -90,6 +103,7 @@ export interface LanguageServiceDefaults { readonly modeConfiguration: ModeConfiguration; preprocessCode: PreprocessCode | null; completionService: CompletionService; + completionSnippets: CompletionSnippet[]; triggerCharacters: string[]; setModeConfiguration(modeConfiguration: ModeConfiguration): void; } @@ -126,6 +140,10 @@ export class LanguageServiceDefaultsImpl implements LanguageServiceDefaults { return this._modeConfiguration.completionItems.completionService; } + get completionSnippets(): CompletionSnippet[] { + return this._modeConfiguration.completionItems.snippets; + } + get triggerCharacters(): string[] { return this._modeConfiguration.completionItems.triggerCharacters; } @@ -149,7 +167,9 @@ export const defaultCompletionService: CompletionService = function ( _model, _position, _context, - suggestions + suggestions, + _entities, + snippets ) { if (!suggestions) { return Promise.resolve([]); @@ -162,11 +182,22 @@ export const defaultCompletionService: CompletionService = function ( detail: 'keyword' })); - return Promise.resolve(keywordsCompletionItems); + const snippetCompletionItems: ICompletionItem[] = + snippets?.map((item) => ({ + label: item.label || item.prefix, + kind: languages.CompletionItemKind.Snippet, + filterText: item.prefix, + insertText: item.insertText, + insertTextRules: languages.CompletionItemInsertTextRule.InsertAsSnippet, + detail: item.description || 'snippet' + })) || []; + + return Promise.resolve([...keywordsCompletionItems, ...snippetCompletionItems]); }; export const modeConfigurationDefault: Required = { completionItems: { + snippets: [], enable: true, completionService: defaultCompletionService, triggerCharacters: ['.', ' '] diff --git a/src/setupLanguageFeatures.ts b/src/setupLanguageFeatures.ts index bdaeb0f6..743cd3a3 100644 --- a/src/setupLanguageFeatures.ts +++ b/src/setupLanguageFeatures.ts @@ -8,6 +8,7 @@ import { } from './monaco.contribution'; import { languages, IDisposable } from './fillers/monaco-editor-core'; import { LanguageIdEnum } from './common/constants'; +import * as snippets from './snippets'; export interface FeatureConfiguration { /** @@ -80,6 +81,27 @@ export function setupLanguageFeatures( } } +function getDefaultSnippets(languageId: LanguageIdEnum) { + switch (languageId) { + case LanguageIdEnum.HIVE: + return snippets.hiveSnippets; + case LanguageIdEnum.FLINK: + return snippets.flinkSnippets; + case LanguageIdEnum.IMPALA: + return snippets.impalaSnippets; + case LanguageIdEnum.MYSQL: + return snippets.mysqlSnippets; + case LanguageIdEnum.PG: + return snippets.pgsqlSnippets; + case LanguageIdEnum.SPARK: + return snippets.sparkSnippets; + case LanguageIdEnum.TRINO: + return snippets.trinoSnippets; + default: + return []; + } +} + function processConfiguration( languageId: LanguageIdEnum, configuration: FeatureConfiguration @@ -111,12 +133,20 @@ function processConfiguration( : (defaults?.modeConfiguration.completionItems.triggerCharacters ?? modeConfigurationDefault.completionItems.triggerCharacters); + const snippets = + typeof configuration.completionItems !== 'boolean' && + Array.isArray(configuration.completionItems?.snippets) + ? configuration.completionItems!.snippets + : (defaults?.modeConfiguration.completionItems.snippets ?? + getDefaultSnippets(languageId)); + return { diagnostics, completionItems: { enable: completionEnable, completionService, - triggerCharacters + triggerCharacters, + snippets } }; } diff --git a/src/snippets.ts b/src/snippets.ts new file mode 100644 index 00000000..997e8f28 --- /dev/null +++ b/src/snippets.ts @@ -0,0 +1,7 @@ +export { hiveSnippets } from './languages/hive/hive.snippet'; +export { flinkSnippets } from './languages/flink/flink.snippet'; +export { trinoSnippets } from './languages/trino/trino.snippet'; +export { pgsqlSnippets } from './languages/pgsql/pgsql.snippet'; +export { sparkSnippets } from './languages/spark/spark.snippet'; +export { mysqlSnippets } from './languages/mysql/mysql.snippet'; +export { impalaSnippets } from './languages/impala/impala.snippet'; diff --git a/website/src/languages/helpers/completionService.ts b/website/src/languages/helpers/completionService.ts index c6fde960..e1d640ef 100644 --- a/website/src/languages/helpers/completionService.ts +++ b/website/src/languages/helpers/completionService.ts @@ -16,7 +16,9 @@ export const completionService: CompletionService = async function ( model, _position, _completionContext, - suggestions + suggestions, + _entities, + snippets ) { if (!suggestions) { return Promise.resolve([]); @@ -183,5 +185,17 @@ export const completionService: CompletionService = async function ( } } } - return [...syntaxCompletionItems, ...keywordsCompletionItems]; + + const snippetCompletionItems: ICompletionItem[] = + snippets?.map((item) => ({ + label: item.label || item.prefix, + kind: languages.CompletionItemKind.Snippet, + filterText: item.prefix, + insertText: item.insertText, + insertTextRules: languages.CompletionItemInsertTextRule.InsertAsSnippet, + sortText: '1' + item.prefix, + detail: item.description !== undefined ? item.description : 'SQL模板' + })) || []; + + return [...syntaxCompletionItems, ...keywordsCompletionItems, ...snippetCompletionItems]; }; diff --git a/website/src/languages/index.ts b/website/src/languages/index.ts index 88c0ccbb..5f64c296 100644 --- a/website/src/languages/index.ts +++ b/website/src/languages/index.ts @@ -2,7 +2,6 @@ import 'monaco-sql-languages/esm/all.contributions.js'; import './languageWorker'; import './theme'; import { setupLanguageFeatures, LanguageIdEnum } from 'monaco-sql-languages/esm/main.js'; - import { completionService } from './helpers/completionService'; /**