Skip to content

Commit

Permalink
LS integration for completion
Browse files Browse the repository at this point in the history
  • Loading branch information
nighca committed Dec 26, 2024
1 parent c4859cf commit 4bc7b6a
Show file tree
Hide file tree
Showing 6 changed files with 126 additions and 46 deletions.
107 changes: 77 additions & 30 deletions spx-gui/src/components/editor/code-editor/code-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,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 @@ -53,18 +57,10 @@ import {
getTextDocumentId,
containsPosition
} from './common'
import * as spxDocumentationItems from './document-base/spx'
import * as gopDocumentationItems from './document-base/gop'
import { isDocumentLinkForResourceReference } from './lsp/spxls/methods'
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: []
Expand Down Expand Up @@ -245,6 +241,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<CompletionItem[]> {
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<CompletionItem>((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) {}

Expand Down Expand Up @@ -454,26 +520,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))
ui.registerContextMenuProvider(new ContextMenuProvider(this.lspClient))
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 @@ -93,7 +93,11 @@ export enum DefinitionKind {
/** Constant definition */
Constant,
/** Package definition */
Package
Package,
/** Type definition */
Type,
/** Unknown definition kind */
Unknown
}

export type DefinitionIdentifier = {
Expand Down
7 changes: 7 additions & 0 deletions spx-gui/src/components/editor/code-editor/lsp/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,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
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
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
39 changes: 27 additions & 12 deletions spx-gui/src/components/editor/code-editor/ui/completion/index.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,32 @@
import { computed, shallowReactive, shallowRef, watch } from 'vue'
import Emitter from '@/utils/emitter'
import {
DefinitionKind,
DefinitionKind as CompletionItemKind,
type BaseContext,
type DefinitionDocumentationString,
type Position,
type ITextDocument,
positionEq
} from '../../common'
import type { CodeEditorUI } from '../code-editor-ui'
import { type monaco } from '../../monaco'
import { fuzzyScoreGracefulAggressive as fuzzyScore, type FuzzyScore } from './fuzzy'
import type { CodeEditorUI } from '../code-editor-ui'
import { makeContentWidgetEl } from '../CodeEditorUI.vue'
import { fuzzyScoreGracefulAggressive as fuzzyScore, type FuzzyScore } from './fuzzy'

export type CompletionContext = BaseContext

export type CompletionItemKind = DefinitionKind
export { CompletionItemKind }

export enum InsertTextFormat {
PlainText,
Snippet
}

export type CompletionItem = {
label: string
kind: CompletionItemKind
insertText: string
insertTextFormat: InsertTextFormat
documentation: DefinitionDocumentationString
}

Expand All @@ -30,7 +36,7 @@ export interface ICompletionProvider {

export type InternalCompletionItem = CompletionItem & {
/** Fuzzy score */
score: FuzzyScore
score: FuzzyScore | null
/** Index in original list */
originalIdx: number
}
Expand Down Expand Up @@ -175,10 +181,14 @@ export class CompletionController extends Emitter<{
if (this.currentCompletion == null) return
const { wordStart, position } = this.currentCompletion
if (!positionEq(cursorPosition, position)) return
await this.ui.insertSnippet(item.insertText, {
start: wordStart,
end: position
})
const range = { start: wordStart, end: position }
switch (item.insertTextFormat) {
case InsertTextFormat.PlainText:
await this.ui.insertText(item.insertText, range)
break
case InsertTextFormat.Snippet:
await this.ui.insertSnippet(item.insertText, range)
}
this.stopCompletion()
editor.focus()
}
Expand Down Expand Up @@ -265,13 +275,17 @@ export class CompletionController extends Emitter<{
}

function shouldTriggerCompletion(char: string) {
return /\w/.test(char)
return /[\w.]/.test(char)
}

function compareItems(a: InternalCompletionItem, b: InternalCompletionItem) {
if (a.score[0] > b.score[0]) {
if (a.score != null && b.score == null) {
return -1
} else if (a.score == null && b.score != null) {
return 1
} else if (a.score != null && b.score !== null && a.score[0] > b.score[0]) {
return -1
} else if (a.score[0] < b.score[0]) {
} else if (a.score != null && b.score !== null && a.score[0] < b.score[0]) {
return 1
} else if (a.originalIdx < b.originalIdx) {
return -1
Expand All @@ -283,6 +297,7 @@ function compareItems(a: InternalCompletionItem, b: InternalCompletionItem) {
}

function filterAndSort(items: CompletionItem[], word: string): InternalCompletionItem[] {
if (word === '') return items.map((item, i) => ({ ...item, score: null, originalIdx: i }))
const wordLow = word.toLowerCase()
const result: InternalCompletionItem[] = []
items.forEach((item, i) => {
Expand Down

0 comments on commit 4bc7b6a

Please sign in to comment.