codemirror-languageservice
Version:
Integrate a Language Server Protocol compatible language service into CodeMirror
229 lines (195 loc) • 6.85 kB
text/typescript
import {
type Completion,
type CompletionSource,
insertCompletionText,
snippet
} from '@codemirror/autocomplete'
import {
type CompletionContext,
type CompletionItem,
type CompletionItemKind,
type CompletionList,
type CompletionTriggerKind,
type InsertTextFormat,
type Position
} from 'vscode-languageserver-protocol'
import { type TextDocument } from 'vscode-languageserver-textdocument'
import { fromMarkupContent } from './markup-content.js'
import { getTextDocument } from './text-document.js'
import { type LSPResult } from './types.js'
let alphabet = 'abcdefghijklmnopqrstuvwxyz'
alphabet += alphabet.toUpperCase()
const defaultFromCompletionItemKind: NonNullable<
createCompletionSource.Options['fromCompletionItemKind']
> = (kind) => {
switch (kind) {
case 1 satisfies typeof CompletionItemKind.Text:
case 15 satisfies typeof CompletionItemKind.Snippet:
return 'text'
case 2 satisfies typeof CompletionItemKind.Method:
return 'method'
case 3 satisfies typeof CompletionItemKind.Function:
return 'function'
case 4 satisfies typeof CompletionItemKind.Constructor:
case 7 satisfies typeof CompletionItemKind.Class:
return 'class'
case 5 satisfies typeof CompletionItemKind.Field:
case 10 satisfies typeof CompletionItemKind.Property:
return 'property'
case 6 satisfies typeof CompletionItemKind.Variable:
case 12 satisfies typeof CompletionItemKind.Value:
case 18 satisfies typeof CompletionItemKind.Reference:
case 23 satisfies typeof CompletionItemKind.Event:
return 'variable'
case 8 satisfies typeof CompletionItemKind.Interface:
case 22 satisfies typeof CompletionItemKind.Struct:
case 25 satisfies typeof CompletionItemKind.TypeParameter:
return 'type'
case 9 satisfies typeof CompletionItemKind.Module:
return 'namespace'
case 13 satisfies typeof CompletionItemKind.Enum:
return 'enum'
case 11 satisfies typeof CompletionItemKind.Unit:
case 14 satisfies typeof CompletionItemKind.Keyword:
case 24 satisfies typeof CompletionItemKind.Operator:
return 'keyword'
case 16 satisfies typeof CompletionItemKind.Color:
case 21 satisfies typeof CompletionItemKind.Constant:
return 'constant'
case 20 satisfies typeof CompletionItemKind.EnumMember:
return 'enum'
default:
}
}
export declare namespace createCompletionSource {
interface Options extends fromMarkupContent.Options {
/**
* Convert an LSP completion item kind to a CodeMirror completion type.
*
* @param kind
* The LSP completion item kind to convert
* @returns
* The CodeMirror completion type.
*/
fromCompletionItemKind?: (kind: CompletionItemKind | undefined) => string | undefined
/**
* Provide LSP completions items.
*
* @param textDocument
* The text document for which to provide completion items.
* @param position
* The position for which to provide completion items.
* @param context
* The completion context.
* @returns
* A completion list, or just the items as an iterable.
*/
doComplete: (
textDocument: TextDocument,
position: Position,
context: CompletionContext
) => LSPResult<CompletionList | Iterable<CompletionItem>>
/**
* The section to use for completions.
*/
section?: string
/**
* Only trigger completions automatically when one of these characters is typed.
*/
triggerCharacters?: string
}
}
/**
* Create an LSP based completion source.
*
* @param options
* Options to configure the completion.
* @returns
* A CodeMirror completion source that uses LSP based completions.
*/
export function createCompletionSource(options: createCompletionSource.Options): CompletionSource {
const fromCompletionItemKind = options.fromCompletionItemKind ?? defaultFromCompletionItemKind
let triggerCharacters = alphabet
if (options.triggerCharacters) {
triggerCharacters += options.triggerCharacters
}
return async (context) => {
const textDocument = getTextDocument(context.state)
let completionContext: CompletionContext
if (context.explicit) {
completionContext = {
triggerKind: 1 satisfies typeof CompletionTriggerKind.Invoked
}
} else {
const triggerCharacter = context.state.sliceDoc(context.pos - 1, context.pos)
if (!triggerCharacters.includes(triggerCharacter)) {
return null
}
completionContext = {
triggerCharacter,
triggerKind: 2 satisfies typeof CompletionTriggerKind.TriggerCharacter
}
}
const completions = await options.doComplete(
textDocument,
textDocument.positionAt(context.pos),
completionContext
)
if (!completions) {
return null
}
if (textDocument.version !== getTextDocument(context.view?.state ?? context.state).version) {
return null
}
let items: Iterable<CompletionItem>
let itemDefaults: CompletionList['itemDefaults']
if (Symbol.iterator in completions) {
items = completions
} else {
items = completions.items
itemDefaults = completions.itemDefaults
}
const completionOptions: Completion[] = []
let minFrom = context.pos
let maxTo = context.pos
for (const item of items) {
const { commitCharacters, detail, documentation, kind, label, textEdit, textEditText } = item
const completion: Completion = {
commitCharacters,
detail,
info:
documentation &&
(() => fromMarkupContent(documentation, document.createDocumentFragment(), options)),
label,
section: options.section,
type: fromCompletionItemKind(kind)
}
if (textEdit) {
const range = 'range' in textEdit ? textEdit.range : textEdit.replace
const from = textDocument.offsetAt(range.start)
const to = textDocument.offsetAt(range.end)
if (from < minFrom) {
minFrom = from
}
if (to > maxTo) {
maxTo = to
}
const insert = textEdit.newText
const insertTextFormat = item.insertTextFormat ?? itemDefaults?.insertTextFormat
completion.apply = (view) =>
insertTextFormat === (2 satisfies typeof InsertTextFormat.Snippet)
? snippet(insert.replaceAll(/\$(\d+)/g, '$${$1}'))(view, completion, from, to)
: view.dispatch(insertCompletionText(view.state, insert, from, to))
} else if (textEditText) {
completion.apply = textEditText
}
completionOptions.push(completion)
}
return {
from: minFrom,
to: maxTo,
commitCharacters: itemDefaults?.commitCharacters,
options: completionOptions
}
}
}