Completion resolving
This commit is contained in:
parent
b058ae8a6f
commit
c2ebaa17b8
|
|
@ -8,11 +8,11 @@ import {
|
|||
LSPClient,
|
||||
LSPPlugin,
|
||||
Workspace,
|
||||
serverCompletion,
|
||||
hoverTooltips,
|
||||
signatureHelp,
|
||||
} from "@codemirror/lsp-client";
|
||||
|
||||
import { serverCompletion } from "./lsp/completion";
|
||||
import { OpenFile } from "./filestate";
|
||||
import { serverDiagnostics } from "./lsp/diagnostics";
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,183 @@
|
|||
import type * as lsp from "vscode-languageserver-protocol"
|
||||
import { EditorState, Extension, Facet } from "@codemirror/state"
|
||||
import { CompletionSource, Completion, CompletionContext, snippet, autocompletion } from "@codemirror/autocomplete"
|
||||
import { LSPPlugin, LSPClientExtension } from "@codemirror/lsp-client"
|
||||
|
||||
/// Register the [language server completion
|
||||
/// source](#lsp-client.serverCompletionSource) as an autocompletion
|
||||
/// source.
|
||||
export function serverCompletion(config: {
|
||||
/// By default, the completion source that asks the language server
|
||||
/// for completions is added as a regular source, in addition to any
|
||||
/// other sources. Set this to true to make it replace all
|
||||
/// completion sources.
|
||||
override?: boolean
|
||||
/// Set a custom
|
||||
/// [`validFor`](#autocomplete.CompletionResult.validFor) expression
|
||||
/// to use in the completion results. By default, the library uses an
|
||||
/// expression that accepts word characters, optionally prefixed by
|
||||
/// any non-word prefixes found in the results.
|
||||
validFor?: RegExp
|
||||
} = {}): LSPClientExtension {
|
||||
let result: Extension[]
|
||||
if (config.override) {
|
||||
result = [autocompletion({ override: [serverCompletionSource] })]
|
||||
} else {
|
||||
let data = [{ autocomplete: serverCompletionSource }]
|
||||
result = [autocompletion(), EditorState.languageData.of(() => data)]
|
||||
}
|
||||
if (config.validFor) result.push(completionConfig.of({ validFor: config.validFor }))
|
||||
return {
|
||||
clientCapabilities: {
|
||||
textDocument: {
|
||||
completion: {
|
||||
completionItem: {
|
||||
snippetSupport: true,
|
||||
// documentationFormat: ["markdown", "plaintext"],
|
||||
resolveSupport: ["documentation"],
|
||||
labelDetailsSupport: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
editorExtension: result,
|
||||
};
|
||||
}
|
||||
|
||||
const completionConfig = Facet.define<{ validFor: RegExp }, { validFor: RegExp | null }>({
|
||||
combine: results => results.length ? results[0] : { validFor: null }
|
||||
})
|
||||
|
||||
function getCompletions(plugin: LSPPlugin, pos: number, context: lsp.CompletionContext, abort?: CompletionContext) {
|
||||
const hasCapability = plugin.client.serverCapabilities ? !!plugin.client.serverCapabilities["completionProvider"] : null;
|
||||
if (hasCapability === false) return Promise.resolve(null)
|
||||
plugin.client.sync()
|
||||
let params: lsp.CompletionParams = {
|
||||
position: plugin.toPosition(pos),
|
||||
textDocument: { uri: plugin.uri },
|
||||
context
|
||||
}
|
||||
if (abort) abort.addEventListener("abort", () => plugin.client.cancelRequest(params))
|
||||
return plugin.client.request<lsp.CompletionParams, lsp.CompletionItem[] | lsp.CompletionList | null>(
|
||||
"textDocument/completion", params)
|
||||
}
|
||||
|
||||
async function resolveDocs(plugin: LSPPlugin, item: lsp.CompletionItem, abort?: CompletionContext): Promise<lsp.CompletionItem> {
|
||||
const hasCapability = plugin.client.serverCapabilities ? !!plugin.client.serverCapabilities["completionProvider"]["resolveProvider"] : null;
|
||||
if (hasCapability === false) {
|
||||
console.warn("No resolving support but also no docs given!");
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
if (abort) abort.addEventListener("abort", () => plugin.client.cancelRequest(item));
|
||||
return await plugin.client.request("completionItem/resolve", item);
|
||||
}
|
||||
|
||||
// Look for non-alphanumeric prefixes in the completions, and return a
|
||||
// regexp that matches them, to use in validFor
|
||||
function prefixRegexp(items: readonly lsp.CompletionItem[]) {
|
||||
let step = Math.ceil(items.length / 50), prefixes: string[] = []
|
||||
for (let i = 0; i < items.length; i += step) {
|
||||
let item = items[i], text = item.textEdit?.newText || item.textEditText || item.insertText || item.label
|
||||
if (!/^\w/.test(text)) {
|
||||
let prefix = /^[^\w]*/.exec(text)![0]
|
||||
if (prefixes.indexOf(prefix) < 0) prefixes.push(prefix)
|
||||
}
|
||||
}
|
||||
if (!prefixes.length) return /^\w*$/
|
||||
return new RegExp("^(?:" + prefixes.map((RegExp as any).escape || (s => s.replace(/[^\w\s]/g, "\\$&"))).join("|") + ")?\\w*$")
|
||||
}
|
||||
|
||||
/// A completion source that requests completions from a language
|
||||
/// server.
|
||||
export const serverCompletionSource: CompletionSource = context => {
|
||||
const plugin = context.view && LSPPlugin.get(context.view)
|
||||
if (!plugin) return null
|
||||
let triggerChar = ""
|
||||
if (!context.explicit) {
|
||||
triggerChar = context.view.state.sliceDoc(context.pos - 1, context.pos)
|
||||
let triggers = plugin.client.serverCapabilities?.completionProvider?.triggerCharacters
|
||||
if (!/[a-zA-Z_]/.test(triggerChar) && !(triggers && triggers.indexOf(triggerChar) > -1)) return null
|
||||
}
|
||||
return getCompletions(plugin, context.pos, {
|
||||
triggerCharacter: triggerChar,
|
||||
triggerKind: context.explicit ? 1 /* Invoked */ : 2 /* TriggerCharacter */
|
||||
}, context).then(result => {
|
||||
if (!result) return null
|
||||
if (Array.isArray(result)) result = { items: result } as lsp.CompletionList
|
||||
let { from, to } = completionResultRange(context, result)
|
||||
let defaultCommitChars = result.itemDefaults?.commitCharacters
|
||||
let config = context.state.facet(completionConfig)
|
||||
|
||||
return {
|
||||
from, to,
|
||||
options: result.items.map((item: lsp.CompletionItem) => {
|
||||
let text = item.textEdit?.newText || item.textEditText || item.insertText || item.label
|
||||
let option: Completion = {
|
||||
label: text,
|
||||
type: item.kind && kindToType[item.kind],
|
||||
}
|
||||
if (item.commitCharacters && item.commitCharacters != defaultCommitChars)
|
||||
option.commitCharacters = item.commitCharacters
|
||||
if (item.detail) option.detail = item.detail; else option.detail = "No details available"
|
||||
if (item.sortText) option.sortText = item.sortText
|
||||
if (item.insertTextFormat == 2 /* Snippet */) {
|
||||
option.apply = (view, c, from, to) => snippet(text)(view, c, from, to)
|
||||
option.label = item.label
|
||||
}
|
||||
option.info = item.documentation ? () => renderDocInfo(plugin, item.documentation!) : async () => {
|
||||
const newItem = await resolveDocs(plugin, item);
|
||||
return newItem.documentation ? renderDocInfo(plugin, newItem.documentation!) : null;
|
||||
}
|
||||
// if (item.documentation) option.info = () => renderDocInfo(plugin, item.documentation!);
|
||||
return option
|
||||
}),
|
||||
commitCharacters: defaultCommitChars,
|
||||
validFor: config.validFor ?? prefixRegexp(result.items),
|
||||
map: (result, changes) => ({ ...result, from: changes.mapPos(result.from) }),
|
||||
}
|
||||
}, err => {
|
||||
if ("code" in err && (err as lsp.ResponseError).code == -32800 /* RequestCancelled */)
|
||||
return null
|
||||
throw err
|
||||
})
|
||||
}
|
||||
|
||||
function completionResultRange(cx: CompletionContext, result: lsp.CompletionList): { from: number, to: number } {
|
||||
if (!result.items.length) return { from: cx.pos, to: cx.pos }
|
||||
let defaultRange = result.itemDefaults?.editRange, item0 = result.items[0]
|
||||
let range = defaultRange ? ("insert" in defaultRange ? defaultRange.insert : defaultRange)
|
||||
: item0.textEdit ? ("range" in item0.textEdit ? item0.textEdit.range : item0.textEdit.insert)
|
||||
: null
|
||||
if (!range) return cx.state.wordAt(cx.pos) || { from: cx.pos, to: cx.pos }
|
||||
let line = cx.state.doc.lineAt(cx.pos)
|
||||
return { from: line.from + range.start.character, to: line.from + range.end.character }
|
||||
}
|
||||
|
||||
function renderDocInfo(plugin: LSPPlugin, doc: string | lsp.MarkupContent) {
|
||||
let elt = document.createElement("div")
|
||||
elt.className = "cm-lsp-documentation cm-lsp-completion-documentation"
|
||||
elt.innerHTML = plugin.docToHTML(doc)
|
||||
return elt
|
||||
}
|
||||
|
||||
const kindToType: { [kind: number]: string } = {
|
||||
1: "text", // Text
|
||||
2: "method", // Method
|
||||
3: "function", // Function
|
||||
4: "class", // Constructor
|
||||
5: "property", // Field
|
||||
6: "variable", // Variable
|
||||
7: "class", // Class
|
||||
8: "interface", // Interface
|
||||
9: "namespace", // Module
|
||||
10: "property", // Property
|
||||
11: "keyword", // Unit
|
||||
12: "constant", // Value
|
||||
13: "constant", // Enum
|
||||
14: "keyword", // Keyword
|
||||
16: "constant", // Color
|
||||
20: "constant", // EnumMember
|
||||
21: "constant", // Constant
|
||||
22: "class", // Struct
|
||||
25: "type" // TypeParameter
|
||||
}
|
||||
|
|
@ -57,8 +57,6 @@ function ensureLspForKey(
|
|||
serverKey,
|
||||
);
|
||||
|
||||
console.log("Current environment: ", process.env);
|
||||
|
||||
// Spawn using shell:true so PATH and shell resolution behave like a user shell.
|
||||
const proc = spawn([cmd].concat(args).join(" "), {
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
|
|
|
|||
Loading…
Reference in New Issue