Skip to content

Commit

Permalink
fix #15: align completion/completionResolve with vscode behaviour
Browse files Browse the repository at this point in the history
Signed-off-by: Anton Kosyakov <[email protected]>
  • Loading branch information
akosyakov committed Aug 6, 2018
1 parent 2e4f81f commit 1cca318
Show file tree
Hide file tree
Showing 8 changed files with 420 additions and 77 deletions.
2 changes: 1 addition & 1 deletion example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"version": "0.2.0",
"dependencies": {
"typescript-language-server": "0.2.0",
"monaco-languageclient": "latest",
"monaco-languageclient": "next",
"express": "^4.15.2",
"normalize-url": "^2.0.1",
"reconnecting-websocket": "^3.2.2",
Expand Down
287 changes: 287 additions & 0 deletions server/src/completion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
/*
* Copyright (C) 2018 TypeFox and others.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*/

import * as lsp from 'vscode-languageserver';
import * as tsp from 'typescript/lib/protocol';
import { LspDocument } from './document';
import { ScriptElementKind } from './tsp-command-types';
import { asRange, toTextEdit } from './protocol-translation';

export interface TSCompletionItem extends lsp.CompletionItem {
data: tsp.CompletionDetailsRequestArgs
}

export function asCompletionItem(entry: import('typescript/lib/protocol').CompletionEntry, file: string, position: lsp.Position, document: LspDocument): TSCompletionItem {
const item: TSCompletionItem = {
label: entry.name,
kind: asCompletionItemKind(entry.kind),
sortText: entry.sortText,
commitCharacters: asCommitCharacters(entry.kind),
data: {
file,
line: position.line + 1,
offset: position.character + 1,
entryNames: [
entry.source ? { name: entry.name, source: entry.source } : entry.name
]
}
}
if (entry.isRecommended) {
// Make sure isRecommended property always comes first
// https://github.com/Microsoft/vscode/issues/40325
item.preselect = true;
} else if (entry.source) {
// De-prioritze auto-imports
// https://github.com/Microsoft/vscode/issues/40311
item.sortText = '\uffff' + entry.sortText;
}
if (item.kind === lsp.CompletionItemKind.Function || item.kind === lsp.CompletionItemKind.Method) {
item.insertTextFormat = lsp.InsertTextFormat.Snippet;
}

let insertText = entry.insertText;
let replacementRange = entry.replacementSpan && asRange(entry.replacementSpan);
// Make sure we only replace a single line at most
if (replacementRange && replacementRange.start.line !== replacementRange.end.line) {
replacementRange = lsp.Range.create(replacementRange.start, document.getLineEnd(replacementRange.start.line));
}
if (insertText && replacementRange && insertText[0] === '[') { // o.x -> o['x']
item.filterText = '.' + item.label;
}
if (entry.kindModifiers && entry.kindModifiers.match(/\boptional\b/)) {
if (!insertText) {
insertText = item.label;
}
if (!item.filterText) {
item.filterText = item.label;
}
item.label += '?';
}
if (insertText && replacementRange) {
item.textEdit = lsp.TextEdit.replace(replacementRange, insertText);
} else {
item.insertText = insertText;
}
return item;
}

export function asCompletionItemKind(kind: ScriptElementKind): lsp.CompletionItemKind {
switch (kind) {
case ScriptElementKind.primitiveType:
case ScriptElementKind.keyword:
return lsp.CompletionItemKind.Keyword;
case ScriptElementKind.constElement:
return lsp.CompletionItemKind.Constant;
case ScriptElementKind.letElement:
case ScriptElementKind.variableElement:
case ScriptElementKind.localVariableElement:
case ScriptElementKind.alias:
return lsp.CompletionItemKind.Variable;
case ScriptElementKind.memberVariableElement:
case ScriptElementKind.memberGetAccessorElement:
case ScriptElementKind.memberSetAccessorElement:
return lsp.CompletionItemKind.Field;
case ScriptElementKind.functionElement:
return lsp.CompletionItemKind.Function;
case ScriptElementKind.memberFunctionElement:
case ScriptElementKind.constructSignatureElement:
case ScriptElementKind.callSignatureElement:
case ScriptElementKind.indexSignatureElement:
return lsp.CompletionItemKind.Method;
case ScriptElementKind.enumElement:
return lsp.CompletionItemKind.Enum;
case ScriptElementKind.moduleElement:
case ScriptElementKind.externalModuleName:
return lsp.CompletionItemKind.Module;
case ScriptElementKind.classElement:
case ScriptElementKind.typeElement:
return lsp.CompletionItemKind.Class;
case ScriptElementKind.interfaceElement:
return lsp.CompletionItemKind.Interface;
case ScriptElementKind.warning:
case ScriptElementKind.scriptElement:
return lsp.CompletionItemKind.File;
case ScriptElementKind.directory:
return lsp.CompletionItemKind.Folder;
case ScriptElementKind.string:
return lsp.CompletionItemKind.Constant;
}
return lsp.CompletionItemKind.Property;
}

export function asCommitCharacters(kind: ScriptElementKind): string[] | undefined {
const commitCharacters: string[] = [];
switch (kind) {
case ScriptElementKind.memberGetAccessorElement:
case ScriptElementKind.memberSetAccessorElement:
case ScriptElementKind.constructSignatureElement:
case ScriptElementKind.callSignatureElement:
case ScriptElementKind.indexSignatureElement:
case ScriptElementKind.enumElement:
case ScriptElementKind.interfaceElement:
commitCharacters.push('.');
break;

case ScriptElementKind.moduleElement:
case ScriptElementKind.alias:
case ScriptElementKind.constElement:
case ScriptElementKind.letElement:
case ScriptElementKind.variableElement:
case ScriptElementKind.localVariableElement:
case ScriptElementKind.memberVariableElement:
case ScriptElementKind.classElement:
case ScriptElementKind.functionElement:
case ScriptElementKind.memberFunctionElement:
commitCharacters.push('.', ',');
commitCharacters.push('(');
break;
}

return commitCharacters.length === 0 ? undefined : commitCharacters;
}

export function asResolvedCompletionItem(item: TSCompletionItem, details: tsp.CompletionEntryDetails): TSCompletionItem {
item.detail = asDetail(details);
item.documentation = asDocumentation(details);
Object.assign(item, asCodeActions(details, item.data.file));
return item;
}

export function asCodeActions(details: tsp.CompletionEntryDetails, filepath: string): {
command?: lsp.Command, additionalTextEdits?: lsp.TextEdit[]
} {
if (!details.codeActions || !details.codeActions.length) {
return {};
}

// Try to extract out the additionalTextEdits for the current file.
// Also check if we still have to apply other workspace edits and commands
// using a vscode command
const additionalTextEdits: lsp.TextEdit[] = [];
let hasReaminingCommandsOrEdits = false;
for (const tsAction of details.codeActions) {
if (tsAction.commands) {
hasReaminingCommandsOrEdits = true;
}

// Apply all edits in the current file using `additionalTextEdits`
if (tsAction.changes) {
for (const change of tsAction.changes) {
if (change.fileName === filepath) {
for (const textChange of change.textChanges) {
additionalTextEdits.push(toTextEdit(textChange));
}
} else {
hasReaminingCommandsOrEdits = true;
}
}
}
}

let command: lsp.Command | undefined = undefined;
if (hasReaminingCommandsOrEdits) {
// Create command that applies all edits not in the current file.
command = {
title: '',
command: '_typescript.applyCompletionCodeAction',
arguments: [filepath, details.codeActions.map(codeAction => ({
commands: codeAction.commands,
description: codeAction.description,
changes: codeAction.changes.filter(x => x.fileName !== filepath)
}))]
};
}

return {
command,
additionalTextEdits: additionalTextEdits.length ? additionalTextEdits : undefined
};
}

export function asDetail({ displayParts, source }: tsp.CompletionEntryDetails): string | undefined {
const result: string[] = [];
const importPath = asPlainText(source);
if (importPath) {
result.push(`Auto import from '${importPath}'`);
}
const detail = asPlainText(displayParts);
if (detail) {
result.push(detail);
}
return result.join('/n');
}

export function asDocumentation(details: tsp.CompletionEntryDetails): lsp.MarkupContent | undefined {
let value = '';
const documentation = asPlainText(details.documentation);
if (documentation) {
value += documentation;
}
if (details.tags) {
const tagsDocumentation = asTagsDocumentation(details.tags);
if (tagsDocumentation) {
value += '\n\n' + tagsDocumentation;
}
}
return value.length ? {
kind: lsp.MarkupKind.Markdown,
value
} : undefined;
}

export function asTagsDocumentation(tags: tsp.JSDocTagInfo[]): string {
return tags.map(asTagDocumentation).join(' \n\n');
}

export function asTagDocumentation(tag: tsp.JSDocTagInfo): string {
switch (tag.name) {
case 'param':
const body = (tag.text || '').split(/^([\w\.]+)\s*-?\s*/);
if (body && body.length === 3) {
const param = body[1];
const doc = body[2];
const label = `*@${tag.name}* \`${param}\``;
if (!doc) {
return label;
}
return label + (doc.match(/\r\n|\n/g) ? ' \n' + doc : ` — ${doc}`);
}
}

// Generic tag
const label = `*@${tag.name}*`;
const text = asTagBodyText(tag);
if (!text) {
return label;
}
return label + (text.match(/\r\n|\n/g) ? ' \n' + text : ` — ${text}`);
}

export function asTagBodyText(tag: tsp.JSDocTagInfo): string | undefined {
if (!tag.text) {
return undefined;
}

switch (tag.name) {
case 'example':
case 'default':
// Convert to markdown code block if it not already one
if (tag.text.match(/^\s*[~`]{3}/g)) {
return tag.text;
}
return '```\n' + tag.text + '\n```';
}

return tag.text;
}

export function asPlainText(parts?: tsp.SymbolDisplayPart[]): string | undefined {
if (!parts ||  !parts.length) {
return undefined;
}
return parts.map(part => part.text).join('');
}
4 changes: 2 additions & 2 deletions server/src/lsp-server.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ describe('completion', () => {
textDocument: doc,
position: pos
});
assert.isTrue(proposals.items.length > 800);
const item = proposals.items.filter(i => i.label === 'addEventListener')[0];
assert.isTrue(proposals.length > 800);
const item = proposals.filter(i => i.label === 'addEventListener')[0];
const resolvedItem = await server.completionResolve(item)
assert.isTrue(resolvedItem.documentation !== undefined);
server.didCloseTextDocument({
Expand Down
Loading

0 comments on commit 1cca318

Please sign in to comment.