Skip to content

Commit

Permalink
LS integration for completion (#1178)
Browse files Browse the repository at this point in the history
  • Loading branch information
nighca authored Dec 26, 2024
1 parent 4308b3d commit a6b1cef
Show file tree
Hide file tree
Showing 12 changed files with 189 additions and 54 deletions.
129 changes: 98 additions & 31 deletions spx-gui/src/components/editor/code-editor/code-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Disposable } from '@/utils/disposable'
import Emitter from '@/utils/emitter'
import { insertSpaces, tabSize } from '@/utils/spx/highlighter'
import type { I18n } from '@/utils/i18n'
import { packageSpx } from '@/utils/spx'
import type { Runtime } from '@/models/runtime'
import type { Project } from '@/models/project'
import { Copilot } from './copilot'
Expand All @@ -24,15 +25,19 @@ 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,
type DefinitionDocumentationItem,
type DefinitionDocumentationString,
type Diagnostic,
makeAdvancedMarkdownString,
stringifyDefinitionId,
selection2Range,
toLSPPosition,
fromLSPRange,
Expand All @@ -49,19 +54,12 @@ import {
type Selection,
type CommandArgs,
getTextDocumentId,
containsPosition
containsPosition,
makeBasicMarkdownString
} 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
Expand Down Expand Up @@ -226,6 +224,94 @@ class HoverProvider implements IHoverProvider {
}
}

class CompletionProvider implements ICompletionProvider {
constructor(
private lspClient: SpxLSPClient,
private documentBase: DocumentBase
) {}

private getCompletionItemKind(kind: lsp.CompletionItemKind | undefined): CompletionItemKind {
switch (kind) {
case lsp.CompletionItemKind.Method:
case lsp.CompletionItemKind.Function:
case lsp.CompletionItemKind.Constructor:
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<CompletionItem[]> {
const items = await this.lspClient.getCompletionItems({
textDocument: ctx.textDocument.id,
position: toLSPPosition(position)
})
const maybeItems = await Promise.all(
items.map(async (item) => {
const result: CompletionItem = {
label: item.label,
kind: this.getCompletionItemKind(item.kind),
insertText: item.label,
insertTextFormat: InsertTextFormat.PlainText,
documentation: null
}

const defId = item.data?.definition
const definition = defId != null ? await this.documentBase.getDocumentation(defId) : null

// Skip APIs from spx while without documentation, they are assumed not recommended
if (defId != null && defId.package === packageSpx && definition == null) return null

if (definition != null) {
result.kind = definition.kind
result.insertText = definition.insertText
result.insertTextFormat = InsertTextFormat.Snippet
result.documentation = makeBasicMarkdownString(definition.overview)
}

if (item.documentation != null) {
const docStr = lsp.MarkupContent.is(item.documentation) ? item.documentation.value : item.documentation
result.documentation = makeAdvancedMarkdownString(docStr)
}

if (item.insertText != null) {
result.insertText = item.insertText
result.insertTextFormat = this.getInsertTextFormat(item.insertTextFormat)
}
return result
})
)
return maybeItems.filter((item) => item != null) as CompletionItem[]
}
}

class ContextMenuProvider implements IContextMenuProvider {
constructor(
private lspClient: SpxLSPClient,
Expand Down Expand Up @@ -435,26 +521,7 @@ export class CodeEditor extends Disposable {
}
})

ui.registerCompletionProvider({
async provideCompletion(ctx, position) {
console.warn('TODO', ctx, position)
await new Promise<void>((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(`
<definition-item overview="${item.overview}" def-id="${stringifyDefinitionId(item.definition)}">
</definition-item>
`)
}))
}
})

ui.registerCompletionProvider(new CompletionProvider(this.lspClient, documentBase))
ui.registerContextMenuProvider(new ContextMenuProvider(lspClient, documentBase))
ui.registerCopilot(copilot)
ui.registerDiagnosticsProvider(this.diagnosticsProvider)
Expand Down
6 changes: 5 additions & 1 deletion spx-gui/src/components/editor/code-editor/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,11 @@ export enum DefinitionKind {
/** Constant definition */
Constant,
/** Package definition */
Package
Package,
/** Type definition */
Type,
/** Unknown definition kind */
Unknown
}

export type DefinitionIdentifier = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { LocaleMessage } from '@/utils/i18n'
import { packageSpx } from '@/utils/spx'
import {
DefinitionKind,
type DefinitionDocumentationItem,
Expand All @@ -7,8 +8,6 @@ import {
type DefinitionDocumentationCategory
} from '../common'

const packageSpx = 'github.com/goplus/spx'

export const clone: DefinitionDocumentationItem = {
categories: [categories.motion.position],
kind: DefinitionKind.Command,
Expand Down
20 changes: 19 additions & 1 deletion spx-gui/src/components/editor/code-editor/lsp/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ import {
import { Spxlc } from './spxls/client'
import type { Files as SpxlsFiles } from './spxls'
import { spxGetDefinitions, spxRenameResources } from './spxls/commands'
import { isDocumentLinkForResourceReference, parseDocumentLinkForDefinition } from './spxls/methods'
import {
type CompletionItem,
isDocumentLinkForResourceReference,
parseDocumentLinkForDefinition
} from './spxls/methods'

function loadScript(url: string) {
return new Promise((resolve, reject) => {
Expand Down Expand Up @@ -127,6 +131,13 @@ export class SpxLSPClient extends Disposable {
return spxlc.request<lsp.Hover | null>(lsp.HoverRequest.method, params)
}

async textDocumentCompletion(
params: lsp.CompletionParams
): Promise<lsp.CompletionList | lsp.CompletionItem[] | null> {
const spxlc = await this.prepareRequest()
return spxlc.request<lsp.CompletionList | lsp.CompletionItem[] | null>(lsp.CompletionRequest.method, params)
}

async textDocumentDefinition(params: lsp.DefinitionParams): Promise<lsp.Definition | null> {
const spxlc = await this.prepareRequest()
return spxlc.request<lsp.Definition | null>(lsp.DefinitionRequest.method, params)
Expand Down Expand Up @@ -181,4 +192,11 @@ export class SpxLSPClient extends Disposable {
}
return null
}

async getCompletionItems(params: lsp.CompletionParams) {
const completionResult = await this.textDocumentCompletion(params)
if (completionResult == null) return []
if (!Array.isArray(completionResult)) return [] // For now, we support CompletionItem[] only
return completionResult as CompletionItem[]
}
}
10 changes: 10 additions & 0 deletions spx-gui/src/components/editor/code-editor/lsp/spxls/methods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,13 @@ export function parseDocumentLinkForDefinition(link: lsp.DocumentLink): Definiti
return null
}
}

/** CompletionItemData represents data in a completion item. */
export type CompletionItemData = {
/** The corresponding definition of the completion item */
definition?: DefinitionIdentifier
}

export interface CompletionItem extends lsp.CompletionItem {
data?: CompletionItemData
}
10 changes: 8 additions & 2 deletions spx-gui/src/components/editor/code-editor/ui/code-editor-ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,14 +237,20 @@ 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:
// 1. remove the range with `executeEdits`
// 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
Expand Down Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, ref, watchEffect } from 'vue'
import { computed, ref, watch, watchEffect } from 'vue'
import { MonacoKeyCode, type monaco } from '../../monaco'
import MarkdownView from '../markdown/MarkdownView.vue'
import CodeEditorCard from '../CodeEditorCard.vue'
Expand All @@ -14,6 +14,10 @@ const props = defineProps<{
const activeIdx = ref(0)
const activeItem = computed<InternalCompletionItem | null>(() => props.items[activeIdx.value] ?? null)
watch(activeItem, (item) => {
if (item == null) activeIdx.value = 0
})
function moveActiveUp() {
const newIdx = activeIdx.value - 1
activeIdx.value = newIdx < 0 ? props.items.length - 1 : newIdx
Expand Down Expand Up @@ -71,8 +75,8 @@ function applyItem(item: InternalCompletionItem) {
@click="applyItem(item)"
/>
</ul>
<div v-if="activeItem != null" class="completion-item-detail">
<MarkdownView v-bind="activeItem.documentation" />
<div class="completion-item-detail">
<MarkdownView v-if="activeItem?.documentation != null" v-bind="activeItem.documentation" />
</div>
</CodeEditorCard>
</template>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -51,6 +51,7 @@ watchEffect(() => {

<style lang="scss" scoped>
.completion-item {
min-width: 8em;
display: flex;
align-items: center;
padding: 8px;
Expand Down
Loading

0 comments on commit a6b1cef

Please sign in to comment.