From 1fba38be58201ed2b842d61f419a4400d05fa2d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=87=8E=E8=BF=82=E8=BF=82?= Date: Thu, 25 May 2023 16:43:08 +0800 Subject: [PATCH] feat: flinksql completion --- src/common/baseSQLWorker.ts | 19 +++ src/common/languageFeatures.ts | 272 +++++++++++++++++++++++++++++++- src/common/setupLanguageMode.ts | 11 +- 3 files changed, 299 insertions(+), 3 deletions(-) diff --git a/src/common/baseSQLWorker.ts b/src/common/baseSQLWorker.ts index 9aabf367..648171d4 100644 --- a/src/common/baseSQLWorker.ts +++ b/src/common/baseSQLWorker.ts @@ -31,6 +31,25 @@ export abstract class BaseSQLWorker { async autocomplete(code: string, position: any): Promise {} + async doComplete(code: string, position: any): Promise { + code = code || this.getTextDocument(); + if (code) { + // TODO: going to do server side search in future, but now just get all keywords for completion + const keywords: string[] = + this.parser + .createParser('') + .symbolicNames?.filter((keyword: string) => keyword?.startsWith('KW_')) + .map((k: string) => k.replace('KW_', '')) || []; + return Promise.resolve({ + items: keywords.map((i) => ({ + label: i, + kind: 14 + })) + }); + } + return Promise.resolve(); + } + private getTextDocument(): string { const model = this._ctx.getMirrorModels()[0]; // When there are multiple files open, this will be an array return model && model.getValue(); diff --git a/src/common/languageFeatures.ts b/src/common/languageFeatures.ts index a7b35329..f2d403c0 100644 --- a/src/common/languageFeatures.ts +++ b/src/common/languageFeatures.ts @@ -1,17 +1,31 @@ import { LanguageServiceDefaults } from './_.contribution'; -import { editor, Uri, IDisposable, MarkerSeverity } from '../fillers/monaco-editor-core'; +import { + editor, + Uri, + IDisposable, + MarkerSeverity, + Range, + languages, + Position, + CancellationToken, + IRange +} from '../fillers/monaco-editor-core'; import { debounce } from './utils'; export interface WorkerAccessor { (first: Uri, ...more: Uri[]): Promise; } -export interface IWorker { +export interface IWorker extends ILanguageWorkerWithCompletions { doValidation(uri: string): Promise; valid(code: string): Promise; parserTreeToString(code: string): Promise; } +export interface ILanguageWorkerWithCompletions { + doComplete(uri: string, position: lsTypes.Position): Promise; +} + export class DiagnosticsAdapter { private _disposables: IDisposable[] = []; private _listener: { [uri: string]: IDisposable } = Object.create(null); @@ -120,3 +134,257 @@ function toDiagnostics(resource: Uri, diag: any): editor.IMarkerData { source: diag.source }; } + +namespace lsTypes { + export type Position = any; + export type CompletionList = { + isIncomplete: boolean; + items: any[]; + }; + + export enum InsertTextFormat { + PlainText = 1, + Snippet = 2 + } + export type Range = { + start: Position; + end: Position; + }; + + export type TextEdit = any; + export type InsertReplaceEdit = any; + export declare namespace CompletionItemKind { + const Text: 1; + const Method: 2; + const Function: 3; + const Constructor: 4; + const Field: 5; + const Variable: 6; + const Class: 7; + const Interface: 8; + const Module: 9; + const Property: 10; + const Unit: 11; + const Value: 12; + const Enum: 13; + const Keyword: 14; + const Snippet: 15; + const Color: 16; + const File: 17; + const Reference: 18; + const Folder: 19; + const EnumMember: 20; + const Constant: 21; + const Struct: 22; + const Event: 23; + const Operator: 24; + const TypeParameter: 25; + } + export declare type CompletionItemKind = + | 1 + | 2 + | 3 + | 4 + | 5 + | 6 + | 7 + | 8 + | 9 + | 10 + | 11 + | 12 + | 13 + | 14 + | 15 + | 16 + | 17 + | 18 + | 19 + | 20 + | 21 + | 22 + | 23 + | 24 + | 25; + export type Command = any; +} + +export class CompletionAdapter implements languages.CompletionItemProvider { + constructor(private readonly _worker: WorkerAccessor) {} + + provideCompletionItems( + model: editor.IReadOnlyModel, + position: Position, + context: languages.CompletionContext, + token: CancellationToken + ): Promise { + const resource = model.uri; + + return this._worker(resource) + .then((worker) => { + return worker.doComplete( + editor.getModel(resource)?.getValue() || '', + fromPosition(position) + ); + }) + .then((info) => { + console.log('info:', info); + if (!info) { + return; + } + const wordInfo = model.getWordUntilPosition(position); + const wordRange = new Range( + position.lineNumber, + wordInfo.startColumn, + position.lineNumber, + wordInfo.endColumn + ); + + const items: languages.CompletionItem[] = info.items.map((entry) => { + const item: languages.CompletionItem = { + label: entry.label, + insertText: entry.insertText || entry.label, + range: wordRange, + kind: toCompletionItemKind(entry.kind) + }; + if (entry.textEdit) { + if (isInsertReplaceEdit(entry.textEdit)) { + item.range = { + insert: toRange(entry.textEdit.insert), + replace: toRange(entry.textEdit.replace) + }; + } else { + item.range = toRange(entry.textEdit.range); + } + item.insertText = entry.textEdit.newText; + } + if (entry.additionalTextEdits) { + item.additionalTextEdits = ( + entry.additionalTextEdits as any[] + ).map(toTextEdit); + } + if (entry.insertTextFormat === lsTypes.InsertTextFormat.Snippet) { + item.insertTextRules = + languages.CompletionItemInsertTextRule.InsertAsSnippet; + } + return item; + }); + + return { + suggestions: items + }; + }); + } +} + +export function fromPosition(position: Position): lsTypes.Position; +export function fromPosition(position: undefined): undefined; +export function fromPosition(position: Position | undefined): lsTypes.Position | undefined; +export function fromPosition(position: Position | undefined): lsTypes.Position | undefined { + if (!position) { + return void 0; + } + return { character: position.column - 1, line: position.lineNumber - 1 }; +} + +export function fromRange(range: IRange): lsTypes.Range; +export function fromRange(range: undefined): undefined; +export function fromRange(range: IRange | undefined): lsTypes.Range | undefined; +export function fromRange(range: IRange | undefined): lsTypes.Range | undefined { + if (!range) { + return void 0; + } + return { + start: { + line: range.startLineNumber - 1, + character: range.startColumn - 1 + }, + end: { line: range.endLineNumber - 1, character: range.endColumn - 1 } + }; +} +export function toRange(range: lsTypes.Range): Range; +export function toRange(range: undefined): undefined; +export function toRange(range: lsTypes.Range | undefined): Range | undefined; +export function toRange(range: lsTypes.Range | undefined): Range | undefined { + if (!range) { + return void 0; + } + return new Range( + range.start.line + 1, + range.start.character + 1, + range.end.line + 1, + range.end.character + 1 + ); +} + +function isInsertReplaceEdit( + edit: lsTypes.TextEdit | lsTypes.InsertReplaceEdit +): edit is lsTypes.InsertReplaceEdit { + return ( + typeof (edit).insert !== 'undefined' && + typeof (edit).replace !== 'undefined' + ); +} + +function toCompletionItemKind(kind: number | undefined): languages.CompletionItemKind { + const mItemKind = languages.CompletionItemKind; + + switch (kind) { + case lsTypes.CompletionItemKind.Text: + return mItemKind.Text; + case lsTypes.CompletionItemKind.Method: + return mItemKind.Method; + case lsTypes.CompletionItemKind.Function: + return mItemKind.Function; + case lsTypes.CompletionItemKind.Constructor: + return mItemKind.Constructor; + case lsTypes.CompletionItemKind.Field: + return mItemKind.Field; + case lsTypes.CompletionItemKind.Variable: + return mItemKind.Variable; + case lsTypes.CompletionItemKind.Class: + return mItemKind.Class; + case lsTypes.CompletionItemKind.Interface: + return mItemKind.Interface; + case lsTypes.CompletionItemKind.Module: + return mItemKind.Module; + case lsTypes.CompletionItemKind.Property: + return mItemKind.Property; + case lsTypes.CompletionItemKind.Unit: + return mItemKind.Unit; + case lsTypes.CompletionItemKind.Value: + return mItemKind.Value; + case lsTypes.CompletionItemKind.Enum: + return mItemKind.Enum; + case lsTypes.CompletionItemKind.Keyword: + return mItemKind.Keyword; + case lsTypes.CompletionItemKind.Snippet: + return mItemKind.Snippet; + case lsTypes.CompletionItemKind.Color: + return mItemKind.Color; + case lsTypes.CompletionItemKind.File: + return mItemKind.File; + case lsTypes.CompletionItemKind.Reference: + return mItemKind.Reference; + } + return mItemKind.Property; +} + +export function toTextEdit(textEdit: lsTypes.TextEdit): languages.TextEdit; +export function toTextEdit(textEdit: undefined): undefined; +export function toTextEdit(textEdit: lsTypes.TextEdit | undefined): languages.TextEdit | undefined; +export function toTextEdit(textEdit: lsTypes.TextEdit | undefined): languages.TextEdit | undefined { + if (!textEdit) { + return void 0; + } + return { + range: toRange(textEdit.range), + text: textEdit.newText + }; +} + +function toCommand(c: lsTypes.Command | undefined): languages.Command | undefined { + return c && c.command === 'editor.action.triggerSuggest' + ? { id: c.command, title: c.title, arguments: c.arguments } + : undefined; +} diff --git a/src/common/setupLanguageMode.ts b/src/common/setupLanguageMode.ts index d98167ab..52418960 100644 --- a/src/common/setupLanguageMode.ts +++ b/src/common/setupLanguageMode.ts @@ -1,7 +1,7 @@ import { WorkerManager } from './workerManager'; import { LanguageServiceDefaults } from './_.contribution'; import * as languageFeatures from './languageFeatures'; -import { Uri, IDisposable } from '../fillers/monaco-editor-core'; +import { Uri, IDisposable, languages } from '../fillers/monaco-editor-core'; export function setupLanguageMode( defaults: LanguageServiceDefaults @@ -24,6 +24,15 @@ export function setupLanguageMode( if (modeConfiguration.diagnostics) { providers.push(new languageFeatures.DiagnosticsAdapter(languageId, worker, defaults)); } + + if (modeConfiguration.completionItems) { + providers.push( + languages.registerCompletionItemProvider( + languageId, + new languageFeatures.CompletionAdapter(worker) + ) + ); + } } registerProviders();