From cae37bd02417a46ed27c771eb63afb6052e8cc31 Mon Sep 17 00:00:00 2001 From: nighca Date: Thu, 26 Dec 2024 16:01:21 +0800 Subject: [PATCH] LS integration for completion --- .../editor/code-editor/code-editor.ts | 107 +++++++++++++----- .../components/editor/code-editor/common.ts | 6 +- .../editor/code-editor/lsp/index.ts | 7 ++ .../editor/code-editor/ui/code-editor-ui.ts | 10 +- .../ui/completion/CompletionItem.vue | 3 +- .../editor/code-editor/ui/completion/index.ts | 39 +++++-- 6 files changed, 126 insertions(+), 46 deletions(-) diff --git a/spx-gui/src/components/editor/code-editor/code-editor.ts b/spx-gui/src/components/editor/code-editor/code-editor.ts index bffad80ba..5b22d14e8 100644 --- a/spx-gui/src/components/editor/code-editor/code-editor.ts +++ b/spx-gui/src/components/editor/code-editor/code-editor.ts @@ -24,7 +24,12 @@ import { type IContextMenuProvider, type IHoverProvider, builtInCommandRename, - type MenuItem + type MenuItem, + type ICompletionProvider, + type CompletionContext, + type CompletionItem, + InsertTextFormat, + CompletionItemKind } from './ui/code-editor-ui' import { type Action, @@ -32,7 +37,6 @@ import { type DefinitionDocumentationString, type Diagnostic, makeAdvancedMarkdownString, - stringifyDefinitionId, selection2Range, toLSPPosition, fromLSPRange, @@ -51,17 +55,9 @@ import { getTextDocumentId, containsPosition } from './common' -import * as spxDocumentationItems from './document-base/spx' -import * as gopDocumentationItems from './document-base/gop' import { TextDocument, createTextDocument } from './text-document' import { type Monaco } from './monaco' -// mock data for test -const allItems = Object.values({ - ...spxDocumentationItems, - ...gopDocumentationItems -}) - class ResourceReferencesProvider extends Emitter<{ didChangeResourceReferences: [] // TODO @@ -226,6 +222,76 @@ class HoverProvider implements IHoverProvider { } } +class CompletionProvider implements ICompletionProvider { + constructor(private lspClient: SpxLSPClient) {} + + private getCompletionItemKind(kind: lsp.CompletionItemKind | undefined): CompletionItemKind { + switch (kind) { + case lsp.CompletionItemKind.Method: + case lsp.CompletionItemKind.Function: + case lsp.CompletionItemKind.Constructor: + // TODO: distinguish Read, Command & Listen + return CompletionItemKind.Function + case lsp.CompletionItemKind.Field: + case lsp.CompletionItemKind.Variable: + case lsp.CompletionItemKind.Property: + return CompletionItemKind.Variable + case lsp.CompletionItemKind.Interface: + case lsp.CompletionItemKind.Enum: + case lsp.CompletionItemKind.Struct: + case lsp.CompletionItemKind.TypeParameter: + return CompletionItemKind.Type + case lsp.CompletionItemKind.Module: + return CompletionItemKind.Package + case lsp.CompletionItemKind.Keyword: + case lsp.CompletionItemKind.Operator: + return CompletionItemKind.Statement + case lsp.CompletionItemKind.EnumMember: + case lsp.CompletionItemKind.Text: + case lsp.CompletionItemKind.Constant: + return CompletionItemKind.Constant + default: + return CompletionItemKind.Unknown + } + } + + private getInsertTextFormat(insertTextFormat: lsp.InsertTextFormat | undefined): InsertTextFormat { + switch (insertTextFormat) { + case lsp.InsertTextFormat.Snippet: + return InsertTextFormat.Snippet + default: + return InsertTextFormat.PlainText + } + } + + async provideCompletion(ctx: CompletionContext, position: Position): Promise { + const items = await this.lspClient.textDocumentCompletion({ + textDocument: ctx.textDocument.id, + position: toLSPPosition(position) + }) + if (items == null) return [] + if (!Array.isArray(items)) return [] // For now, we support CompletionItem[] only + return items.map((item) => { + let document = '' + if (item.documentation != null) { + document = lsp.MarkupContent.is(item.documentation) ? item.documentation.value : item.documentation + } + const result: CompletionItem = { + label: item.label, + kind: this.getCompletionItemKind(item.kind), + insertText: item.label, + insertTextFormat: InsertTextFormat.PlainText, + documentation: makeAdvancedMarkdownString(document) + } + if (item.insertText != null) { + result.insertText = item.insertText + result.insertTextFormat = this.getInsertTextFormat(item.insertTextFormat) + } + return result + }) + } +} + class ContextMenuProvider implements IContextMenuProvider { constructor( private lspClient: SpxLSPClient, @@ -435,26 +501,7 @@ export class CodeEditor extends Disposable { } }) - ui.registerCompletionProvider({ - async provideCompletion(ctx, position) { - console.warn('TODO', ctx, position) - await new Promise((resolve) => setTimeout(resolve, 100)) - ctx.signal.throwIfAborted() - return allItems.map((item) => ({ - label: item.definition - .name!.split('.') - .pop()! - .replace(/^./, (c) => c.toLowerCase()), - kind: item.kind, - insertText: item.insertText, - documentation: makeAdvancedMarkdownString(` - - -`) - })) - } - }) - + ui.registerCompletionProvider(new CompletionProvider(this.lspClient)) ui.registerContextMenuProvider(new ContextMenuProvider(lspClient, documentBase)) ui.registerCopilot(copilot) ui.registerDiagnosticsProvider(this.diagnosticsProvider) diff --git a/spx-gui/src/components/editor/code-editor/common.ts b/spx-gui/src/components/editor/code-editor/common.ts index accabca74..3bd3c2051 100644 --- a/spx-gui/src/components/editor/code-editor/common.ts +++ b/spx-gui/src/components/editor/code-editor/common.ts @@ -99,7 +99,11 @@ export enum DefinitionKind { /** Constant definition */ Constant, /** Package definition */ - Package + Package, + /** Type definition */ + Type, + /** Unknown definition kind */ + Unknown } export type DefinitionIdentifier = { diff --git a/spx-gui/src/components/editor/code-editor/lsp/index.ts b/spx-gui/src/components/editor/code-editor/lsp/index.ts index f6c746196..9281993fd 100644 --- a/spx-gui/src/components/editor/code-editor/lsp/index.ts +++ b/spx-gui/src/components/editor/code-editor/lsp/index.ts @@ -127,6 +127,13 @@ export class SpxLSPClient extends Disposable { return spxlc.request(lsp.HoverRequest.method, params) } + async textDocumentCompletion( + params: lsp.CompletionParams + ): Promise { + const spxlc = await this.prepareRequest() + return spxlc.request(lsp.CompletionRequest.method, params) + } + async textDocumentDefinition(params: lsp.DefinitionParams): Promise { const spxlc = await this.prepareRequest() return spxlc.request(lsp.DefinitionRequest.method, params) diff --git a/spx-gui/src/components/editor/code-editor/ui/code-editor-ui.ts b/spx-gui/src/components/editor/code-editor/ui/code-editor-ui.ts index 313b6b778..681614049 100644 --- a/spx-gui/src/components/editor/code-editor/ui/code-editor-ui.ts +++ b/spx-gui/src/components/editor/code-editor/ui/code-editor-ui.ts @@ -237,6 +237,12 @@ export class CodeEditorUI extends Disposable implements ICodeEditorUI { return this.getTextDocument(this.mainTextDocumentId) } + async insertText(text: string, range: Range) { + const editor = this.editor + const inserting = { range: toMonacoRange(range), text } + editor.executeEdits('insertText', [inserting]) + } + async insertSnippet(snippet: string, range: Range) { const editor = this.editor // `executeEdits` does not support snippet, so we have to split the insertion into two steps: @@ -244,7 +250,7 @@ export class CodeEditorUI extends Disposable implements ICodeEditorUI { // 2. insert the snippet with `snippetController2` if (!isRangeEmpty(range)) { const removing = { range: toMonacoRange(range), text: '' } - editor.executeEdits('snippet', [removing]) + editor.executeEdits('insertSnippet', [removing]) await timeout(0) // NOTE: the timeout is necessary, or the cursor position will be wrong after snippet inserted } // it's strange but it works, see details in https://github.com/Microsoft/monaco-editor/issues/342 @@ -375,7 +381,7 @@ export class CodeEditorUI extends Disposable implements ICodeEditorUI { const selection = editor.getSelection() if (selection == null) return const text = await navigator.clipboard.readText() - editor.executeEdits('editor', [{ range: selection, text }]) + editor.executeEdits('paste', [{ range: selection, text }]) editor.focus() } catch (error) { editor.focus() diff --git a/spx-gui/src/components/editor/code-editor/ui/completion/CompletionItem.vue b/spx-gui/src/components/editor/code-editor/ui/completion/CompletionItem.vue index 9d66f75cb..18cfb8886 100644 --- a/spx-gui/src/components/editor/code-editor/ui/completion/CompletionItem.vue +++ b/spx-gui/src/components/editor/code-editor/ui/completion/CompletionItem.vue @@ -15,7 +15,7 @@ type Part = { } const parts = computed(() => { - const matches = createMatches(props.item.score) + const matches = createMatches(props.item.score ?? undefined) const parts: Part[] = [] let lastEnd = 0 for (const match of matches) { @@ -51,6 +51,7 @@ watchEffect(() => {