From eec50a98f252280916478d56cf900f561c7d7299 Mon Sep 17 00:00:00 2001 From: Qiming Zhao Date: Wed, 15 Apr 2020 16:15:19 +0800 Subject: [PATCH] improve resolved completionItem Closes #137 --- src/server/features/completionItemProvider.ts | 78 ++++++++++++------ src/server/features/documentSymbol.ts | 2 +- .../features/implementationsCodeLens.ts | 2 +- src/server/features/referencesCodeLens.ts | 2 +- src/server/protocol.const.ts | 4 +- src/server/utils/completionItem.ts | 80 ++++++++++++------- 6 files changed, 113 insertions(+), 55 deletions(-) diff --git a/src/server/features/completionItemProvider.ts b/src/server/features/completionItemProvider.ts index a25e882..3ba3a56 100644 --- a/src/server/features/completionItemProvider.ts +++ b/src/server/features/completionItemProvider.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CancellationToken, Command, CompletionContext, CompletionItem, InsertTextFormat, MarkupContent, MarkupKind, Position, TextEdit, CompletionList } from 'vscode-languageserver-protocol' +import { CancellationToken, Command, CompletionContext, Range, CompletionItem, InsertTextFormat, MarkupContent, MarkupKind, Position, TextEdit, CompletionList } from 'vscode-languageserver-protocol' import { TextDocument } from 'vscode-languageserver-textdocument' import { commands, workspace } from 'coc.nvim' import { CompletionItemProvider } from 'coc.nvim/lib/provider' @@ -11,7 +11,7 @@ import * as PConst from '../protocol.const' import { ITypeScriptServiceClient, ServerResponse } from '../typescriptService' import API from '../utils/api' import { applyCodeAction } from '../utils/codeAction' -import { convertCompletionEntry, getParameterListParts } from '../utils/completionItem' +import { DotAccessorContext, convertCompletionEntry, getParameterListParts } from '../utils/completionItem' import * as Previewer from '../utils/previewer' import * as typeConverters from '../utils/typeConverters' import TypingsStatus from '../utils/typingsStatus' @@ -111,8 +111,6 @@ export default class TypeScriptCompletionItemProvider implements CompletionItemP } const { completeOption } = this - const doc = workspace.getDocument(uri) - const args: Proto.CompletionsRequestArgs & { includeAutomaticOptionalChainCompletions?: boolean } = { ...typeConverters.Position.toFileLocationRequestArgs(file, position), includeExternalModuleExports: completeOption.autoImports, @@ -121,9 +119,14 @@ export default class TypeScriptCompletionItemProvider implements CompletionItemP includeAutomaticOptionalChainCompletions: completeOption.includeAutomaticOptionalChainCompletions } - let msg: ReadonlyArray | undefined + let entries: ReadonlyArray | undefined + let dotAccessorContext: DotAccessorContext | undefined let isNewIdentifierLocation = true + let isMemberCompletion = false + let isIncomplete = false + const isInValidCommitCharacterContext = this.isInValidCommitCharacterContext(document, position) + if (this.client.apiVersion.gte(API.v300)) { try { const response = await this.client.interruptGetErr(() => this.client.execute('completionInfo', args, token)) @@ -131,7 +134,20 @@ export default class TypeScriptCompletionItemProvider implements CompletionItemP return null } isNewIdentifierLocation = response.body.isNewIdentifierLocation - msg = response.body.entries + isMemberCompletion = response.body.isMemberCompletion + if (isMemberCompletion) { + const dotMatch = preText.slice(0, position.character).match(/\??\.\s*$/) || undefined + if (dotMatch) { + const range = Range.create({ + line: position.line, + character: position.character - dotMatch.length + }, position) + const text = document.getText(range) + dotAccessorContext = { range, text } + } + } + isIncomplete = (response as any).metadata && (response as any).metadata.isIncomplete + entries = response.body.entries } catch (e) { if (e.message == 'No content available.') { return null @@ -143,11 +159,11 @@ export default class TypeScriptCompletionItemProvider implements CompletionItemP if (response.type !== 'response' || !response.body) { return null } - msg = response.body + entries = response.body } const completionItems: CompletionItem[] = [] - for (const element of msg) { + for (const element of entries) { if (shouldExcludeCompletionEntry(element, completeOption)) { continue } @@ -155,21 +171,17 @@ export default class TypeScriptCompletionItemProvider implements CompletionItemP element, uri, position, - completeOption.completeFunctionCalls, - isNewIdentifierLocation + { + isNewIdentifierLocation, + isMemberCompletion, + enableCallCompletions: completeOption.completeFunctionCalls, + isInValidCommitCharacterContext, + dotAccessorContext, + } ) completionItems.push(item) } - let startcol: number | null = null - if (triggerCharacter == '@' && !doc.isWord('@')) { - startcol = option.col - 1 - } - let res: any = { - startcol, - isIncomplete: false, - items: completionItems - } - return res + return { isIncomplete, items: completionItems } } private getTsTriggerCharacter(context: CompletionContext): Proto.CompletionsTriggerCharacter | undefined { @@ -229,10 +241,9 @@ export default class TypeScriptCompletionItemProvider implements CompletionItemP return item } const detail = details[0] - item.detail = detail.displayParts.length - ? Previewer.plain(detail.displayParts) - : undefined - + if (!item.detail && detail.displayParts.length) { + item.detail = Previewer.plain(detail.displayParts) + } item.documentation = this.getDocumentation(detail) const { command, additionalTextEdits } = this.getCodeActions(detail, filepath) if (command) item.command = command @@ -243,6 +254,7 @@ export default class TypeScriptCompletionItemProvider implements CompletionItemP this.createSnippetOfFunctionCall(item, detail) } } + return item } @@ -390,6 +402,24 @@ export default class TypeScriptCompletionItemProvider implements CompletionItemP return true } } + + private isInValidCommitCharacterContext( + document: TextDocument, + position: Position + ): boolean { + if (this.client.apiVersion.lt(API.v320)) { + // Workaround for https://github.com/Microsoft/TypeScript/issues/27742 + // Only enable dot completions when previous character not a dot preceded by whitespace. + // Prevents incorrectly completing while typing spread operators. + if (position.character > 1) { + const preText = document.getText(Range.create( + position.line, 0, + position.line, position.character)) + return preText.match(/(\s|^)\.$/ig) === null + } + } + return true + } } function shouldExcludeCompletionEntry( diff --git a/src/server/features/documentSymbol.ts b/src/server/features/documentSymbol.ts index c88f6e3..3029c54 100644 --- a/src/server/features/documentSymbol.ts +++ b/src/server/features/documentSymbol.ts @@ -20,7 +20,7 @@ const getSymbolKind = (kind: string): SymbolKind => { return SymbolKind.Enum case PConst.Kind.interface: return SymbolKind.Interface - case PConst.Kind.memberFunction: + case PConst.Kind.method: return SymbolKind.Method case PConst.Kind.memberVariable: return SymbolKind.Property diff --git a/src/server/features/implementationsCodeLens.ts b/src/server/features/implementationsCodeLens.ts index d39a701..42210d1 100644 --- a/src/server/features/implementationsCodeLens.ts +++ b/src/server/features/implementationsCodeLens.ts @@ -87,7 +87,7 @@ export default class TypeScriptImplementationsCodeLensProvider extends TypeScrip return super.getSymbolRange(document, item) case PConst.Kind.class: - case PConst.Kind.memberFunction: + case PConst.Kind.method: case PConst.Kind.memberVariable: case PConst.Kind.memberGetAccessor: case PConst.Kind.memberSetAccessor: diff --git a/src/server/features/referencesCodeLens.ts b/src/server/features/referencesCodeLens.ts index 8dae43d..4b56a7f 100644 --- a/src/server/features/referencesCodeLens.ts +++ b/src/server/features/referencesCodeLens.ts @@ -87,7 +87,7 @@ export default class TypeScriptReferencesCodeLensProvider extends TypeScriptBase } // fallthrough - case PConst.Kind.memberFunction: + case PConst.Kind.method: case PConst.Kind.memberVariable: case PConst.Kind.memberGetAccessor: case PConst.Kind.memberSetAccessor: diff --git a/src/server/protocol.const.ts b/src/server/protocol.const.ts index 1283563..bff786b 100644 --- a/src/server/protocol.const.ts +++ b/src/server/protocol.const.ts @@ -12,6 +12,7 @@ export class Kind { public static readonly constructSignature = 'construct' public static readonly directory = 'directory' public static readonly enum = 'enum' + public static readonly enumMember = 'enum member' public static readonly externalModuleName = 'external module name' public static readonly function = 'function' public static readonly indexSignature = 'index' @@ -20,7 +21,7 @@ export class Kind { public static readonly let = 'let' public static readonly localFunction = 'local function' public static readonly localVariable = 'local var' - public static readonly memberFunction = 'method' + public static readonly method = 'method' public static readonly memberGetAccessor = 'getter' public static readonly memberSetAccessor = 'setter' public static readonly memberVariable = 'property' @@ -32,6 +33,7 @@ export class Kind { public static readonly warning = 'warning' public static readonly string = 'string' public static readonly parameter = 'parameter' + public static readonly typeParameter = 'type parameter' } export class DiagnosticCategory { diff --git a/src/server/utils/completionItem.ts b/src/server/utils/completionItem.ts index eaca77e..08d6bd6 100644 --- a/src/server/utils/completionItem.ts +++ b/src/server/utils/completionItem.ts @@ -2,56 +2,57 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { workspace } from 'coc.nvim' import { Range, CompletionItem, CompletionItemKind, InsertTextFormat, Position, TextEdit } from 'vscode-languageserver-protocol' import * as Proto from '../protocol' import * as PConst from '../protocol.const' -interface CommitCharactersSettings { - readonly isNewIdentifierLocation: boolean - readonly isInValidCommitCharacterContext: boolean - readonly useCodeSnippetsOnMethodSuggest: boolean -} - interface ParamterListParts { readonly parts: ReadonlyArray readonly hasOptionalParameters: boolean } +export interface DotAccessorContext { + readonly range: Range + readonly text: string +} + +export interface CompletionContext { + readonly isNewIdentifierLocation: boolean + readonly isMemberCompletion: boolean + readonly isInValidCommitCharacterContext: boolean + readonly enableCallCompletions: boolean + readonly dotAccessorContext?: DotAccessorContext +} + export function convertCompletionEntry( tsEntry: Proto.CompletionEntry, uri: string, position: Position, - useCodeSnippetsOnMethodSuggest: boolean, - isNewIdentifierLocation: boolean + context: CompletionContext, ): CompletionItem { let label = tsEntry.name let sortText = tsEntry.sortText + let preselect = false + let detail: string if (tsEntry.isRecommended) { - // Make sure isRecommended property always comes first - // https://github.com/Microsoft/vscode/issues/40325 - sortText = '\0' + sortText - } else if (tsEntry.source) { - // De-prioritze auto-imports - // https://github.com/Microsoft/vscode/issues/40311 + preselect = true + } + if (tsEntry.source) { + // De-prioritze auto-imports https://github.com/Microsoft/vscode/issues/40311 sortText = '\uffff' + sortText } else { sortText = tsEntry.sortText } let kind = convertKind(tsEntry.kind) let insertTextFormat = ( - useCodeSnippetsOnMethodSuggest && + context.enableCallCompletions && (kind === CompletionItemKind.Function || kind === CompletionItemKind.Method) ) ? InsertTextFormat.Snippet : InsertTextFormat.PlainText let insertText = tsEntry.insertText - let document = workspace.getDocument(uri) - let preText = document.getline(position.line).slice(0, position.character) - const isInValidCommitCharacterContext = preText.match(/(^|[a-z_$\(\)\[\]\{\}]|[^.]\.)\s*$/ig) !== null + let commitCharacters = getCommitCharacters(tsEntry, context) - let commitCharacters = getCommitCharacters(tsEntry, { isNewIdentifierLocation, isInValidCommitCharacterContext, useCodeSnippetsOnMethodSuggest }) - let optional = tsEntry.kindModifiers && tsEntry.kindModifiers.match(/\boptional\b/) let textEdit: TextEdit | null = null if (tsEntry.replacementSpan) { let { start, end } = tsEntry.replacementSpan @@ -62,17 +63,41 @@ export function convertCompletionEntry( } } } + if (tsEntry.kindModifiers) { + const kindModifiers = new Set(tsEntry.kindModifiers.split(/,|\s+/g)) + if (kindModifiers.has(PConst.KindModifiers.optional)) { + label += '?' + } + + if (kindModifiers.has(PConst.KindModifiers.color)) { + kind = CompletionItemKind.Color + } + + if (tsEntry.kind === PConst.Kind.script) { + for (const extModifier of PConst.KindModifiers.fileExtensionKindModifiers) { + if (kindModifiers.has(extModifier)) { + if (tsEntry.name.toLowerCase().endsWith(extModifier)) { + detail = tsEntry.name + } else { + detail = tsEntry.name + extModifier + } + break + } + } + } + } return { label, insertText, textEdit, kind, + preselect, insertTextFormat, sortText, commitCharacters, + detail, data: { uri, - optional, position, source: tsEntry.source || '' } @@ -97,7 +122,7 @@ function convertKind(kind: string): CompletionItemKind { return CompletionItemKind.Field case PConst.Kind.function: return CompletionItemKind.Function - case PConst.Kind.memberFunction: + case PConst.Kind.method: case PConst.Kind.constructSignature: case PConst.Kind.callSignature: case PConst.Kind.indexSignature: @@ -121,8 +146,8 @@ function convertKind(kind: string): CompletionItemKind { return CompletionItemKind.Variable } -function getCommitCharacters(tsEntry: Proto.CompletionEntry, settings: CommitCharactersSettings): string[] | undefined { - if (settings.isNewIdentifierLocation || !settings.isInValidCommitCharacterContext) { +function getCommitCharacters(tsEntry: Proto.CompletionEntry, context: CompletionContext): string[] | undefined { + if (context.isNewIdentifierLocation || !context.isInValidCommitCharacterContext) { return undefined } const commitCharacters: string[] = [] @@ -145,10 +170,11 @@ function getCommitCharacters(tsEntry: Proto.CompletionEntry, settings: CommitCha case PConst.Kind.memberVariable: case PConst.Kind.class: case PConst.Kind.function: - case PConst.Kind.memberFunction: + case PConst.Kind.method: case PConst.Kind.keyword: + case PConst.Kind.parameter: commitCharacters.push('.', ',', ';') - if (settings.useCodeSnippetsOnMethodSuggest) { + if (context.enableCallCompletions) { commitCharacters.push('(') } break