improve resolved completionItem

Closes #137
This commit is contained in:
Qiming Zhao 2020-04-15 16:15:19 +08:00
parent 17fdfd9462
commit eec50a98f2
6 changed files with 113 additions and 55 deletions

View file

@ -2,7 +2,7 @@
* Copyright (c) Microsoft Corporation. All rights reserved. * Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information. * 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 { TextDocument } from 'vscode-languageserver-textdocument'
import { commands, workspace } from 'coc.nvim' import { commands, workspace } from 'coc.nvim'
import { CompletionItemProvider } from 'coc.nvim/lib/provider' import { CompletionItemProvider } from 'coc.nvim/lib/provider'
@ -11,7 +11,7 @@ import * as PConst from '../protocol.const'
import { ITypeScriptServiceClient, ServerResponse } from '../typescriptService' import { ITypeScriptServiceClient, ServerResponse } from '../typescriptService'
import API from '../utils/api' import API from '../utils/api'
import { applyCodeAction } from '../utils/codeAction' 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 Previewer from '../utils/previewer'
import * as typeConverters from '../utils/typeConverters' import * as typeConverters from '../utils/typeConverters'
import TypingsStatus from '../utils/typingsStatus' import TypingsStatus from '../utils/typingsStatus'
@ -111,8 +111,6 @@ export default class TypeScriptCompletionItemProvider implements CompletionItemP
} }
const { completeOption } = this const { completeOption } = this
const doc = workspace.getDocument(uri)
const args: Proto.CompletionsRequestArgs & { includeAutomaticOptionalChainCompletions?: boolean } = { const args: Proto.CompletionsRequestArgs & { includeAutomaticOptionalChainCompletions?: boolean } = {
...typeConverters.Position.toFileLocationRequestArgs(file, position), ...typeConverters.Position.toFileLocationRequestArgs(file, position),
includeExternalModuleExports: completeOption.autoImports, includeExternalModuleExports: completeOption.autoImports,
@ -121,9 +119,14 @@ export default class TypeScriptCompletionItemProvider implements CompletionItemP
includeAutomaticOptionalChainCompletions: completeOption.includeAutomaticOptionalChainCompletions includeAutomaticOptionalChainCompletions: completeOption.includeAutomaticOptionalChainCompletions
} }
let msg: ReadonlyArray<Proto.CompletionEntry> | undefined let entries: ReadonlyArray<Proto.CompletionEntry> | undefined
let dotAccessorContext: DotAccessorContext | undefined
let isNewIdentifierLocation = true let isNewIdentifierLocation = true
let isMemberCompletion = false
let isIncomplete = false
const isInValidCommitCharacterContext = this.isInValidCommitCharacterContext(document, position)
if (this.client.apiVersion.gte(API.v300)) { if (this.client.apiVersion.gte(API.v300)) {
try { try {
const response = await this.client.interruptGetErr(() => this.client.execute('completionInfo', args, token)) const response = await this.client.interruptGetErr(() => this.client.execute('completionInfo', args, token))
@ -131,7 +134,20 @@ export default class TypeScriptCompletionItemProvider implements CompletionItemP
return null return null
} }
isNewIdentifierLocation = response.body.isNewIdentifierLocation 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) { } catch (e) {
if (e.message == 'No content available.') { if (e.message == 'No content available.') {
return null return null
@ -143,11 +159,11 @@ export default class TypeScriptCompletionItemProvider implements CompletionItemP
if (response.type !== 'response' || !response.body) { if (response.type !== 'response' || !response.body) {
return null return null
} }
msg = response.body entries = response.body
} }
const completionItems: CompletionItem[] = [] const completionItems: CompletionItem[] = []
for (const element of msg) { for (const element of entries) {
if (shouldExcludeCompletionEntry(element, completeOption)) { if (shouldExcludeCompletionEntry(element, completeOption)) {
continue continue
} }
@ -155,21 +171,17 @@ export default class TypeScriptCompletionItemProvider implements CompletionItemP
element, element,
uri, uri,
position, position,
completeOption.completeFunctionCalls, {
isNewIdentifierLocation isNewIdentifierLocation,
isMemberCompletion,
enableCallCompletions: completeOption.completeFunctionCalls,
isInValidCommitCharacterContext,
dotAccessorContext,
}
) )
completionItems.push(item) completionItems.push(item)
} }
let startcol: number | null = null return { isIncomplete, items: completionItems }
if (triggerCharacter == '@' && !doc.isWord('@')) {
startcol = option.col - 1
}
let res: any = {
startcol,
isIncomplete: false,
items: completionItems
}
return res
} }
private getTsTriggerCharacter(context: CompletionContext): Proto.CompletionsTriggerCharacter | undefined { private getTsTriggerCharacter(context: CompletionContext): Proto.CompletionsTriggerCharacter | undefined {
@ -229,10 +241,9 @@ export default class TypeScriptCompletionItemProvider implements CompletionItemP
return item return item
} }
const detail = details[0] const detail = details[0]
item.detail = detail.displayParts.length if (!item.detail && detail.displayParts.length) {
? Previewer.plain(detail.displayParts) item.detail = Previewer.plain(detail.displayParts)
: undefined }
item.documentation = this.getDocumentation(detail) item.documentation = this.getDocumentation(detail)
const { command, additionalTextEdits } = this.getCodeActions(detail, filepath) const { command, additionalTextEdits } = this.getCodeActions(detail, filepath)
if (command) item.command = command if (command) item.command = command
@ -243,6 +254,7 @@ export default class TypeScriptCompletionItemProvider implements CompletionItemP
this.createSnippetOfFunctionCall(item, detail) this.createSnippetOfFunctionCall(item, detail)
} }
} }
return item return item
} }
@ -390,6 +402,24 @@ export default class TypeScriptCompletionItemProvider implements CompletionItemP
return true 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( function shouldExcludeCompletionEntry(

View file

@ -20,7 +20,7 @@ const getSymbolKind = (kind: string): SymbolKind => {
return SymbolKind.Enum return SymbolKind.Enum
case PConst.Kind.interface: case PConst.Kind.interface:
return SymbolKind.Interface return SymbolKind.Interface
case PConst.Kind.memberFunction: case PConst.Kind.method:
return SymbolKind.Method return SymbolKind.Method
case PConst.Kind.memberVariable: case PConst.Kind.memberVariable:
return SymbolKind.Property return SymbolKind.Property

View file

@ -87,7 +87,7 @@ export default class TypeScriptImplementationsCodeLensProvider extends TypeScrip
return super.getSymbolRange(document, item) return super.getSymbolRange(document, item)
case PConst.Kind.class: case PConst.Kind.class:
case PConst.Kind.memberFunction: case PConst.Kind.method:
case PConst.Kind.memberVariable: case PConst.Kind.memberVariable:
case PConst.Kind.memberGetAccessor: case PConst.Kind.memberGetAccessor:
case PConst.Kind.memberSetAccessor: case PConst.Kind.memberSetAccessor:

View file

@ -87,7 +87,7 @@ export default class TypeScriptReferencesCodeLensProvider extends TypeScriptBase
} }
// fallthrough // fallthrough
case PConst.Kind.memberFunction: case PConst.Kind.method:
case PConst.Kind.memberVariable: case PConst.Kind.memberVariable:
case PConst.Kind.memberGetAccessor: case PConst.Kind.memberGetAccessor:
case PConst.Kind.memberSetAccessor: case PConst.Kind.memberSetAccessor:

View file

@ -12,6 +12,7 @@ export class Kind {
public static readonly constructSignature = 'construct' public static readonly constructSignature = 'construct'
public static readonly directory = 'directory' public static readonly directory = 'directory'
public static readonly enum = 'enum' public static readonly enum = 'enum'
public static readonly enumMember = 'enum member'
public static readonly externalModuleName = 'external module name' public static readonly externalModuleName = 'external module name'
public static readonly function = 'function' public static readonly function = 'function'
public static readonly indexSignature = 'index' public static readonly indexSignature = 'index'
@ -20,7 +21,7 @@ export class Kind {
public static readonly let = 'let' public static readonly let = 'let'
public static readonly localFunction = 'local function' public static readonly localFunction = 'local function'
public static readonly localVariable = 'local var' public static readonly localVariable = 'local var'
public static readonly memberFunction = 'method' public static readonly method = 'method'
public static readonly memberGetAccessor = 'getter' public static readonly memberGetAccessor = 'getter'
public static readonly memberSetAccessor = 'setter' public static readonly memberSetAccessor = 'setter'
public static readonly memberVariable = 'property' public static readonly memberVariable = 'property'
@ -32,6 +33,7 @@ export class Kind {
public static readonly warning = 'warning' public static readonly warning = 'warning'
public static readonly string = 'string' public static readonly string = 'string'
public static readonly parameter = 'parameter' public static readonly parameter = 'parameter'
public static readonly typeParameter = 'type parameter'
} }
export class DiagnosticCategory { export class DiagnosticCategory {

View file

@ -2,56 +2,57 @@
* Copyright (c) Microsoft Corporation. All rights reserved. * Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information. * 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 { Range, CompletionItem, CompletionItemKind, InsertTextFormat, Position, TextEdit } from 'vscode-languageserver-protocol'
import * as Proto from '../protocol' import * as Proto from '../protocol'
import * as PConst from '../protocol.const' import * as PConst from '../protocol.const'
interface CommitCharactersSettings {
readonly isNewIdentifierLocation: boolean
readonly isInValidCommitCharacterContext: boolean
readonly useCodeSnippetsOnMethodSuggest: boolean
}
interface ParamterListParts { interface ParamterListParts {
readonly parts: ReadonlyArray<Proto.SymbolDisplayPart> readonly parts: ReadonlyArray<Proto.SymbolDisplayPart>
readonly hasOptionalParameters: boolean 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( export function convertCompletionEntry(
tsEntry: Proto.CompletionEntry, tsEntry: Proto.CompletionEntry,
uri: string, uri: string,
position: Position, position: Position,
useCodeSnippetsOnMethodSuggest: boolean, context: CompletionContext,
isNewIdentifierLocation: boolean
): CompletionItem { ): CompletionItem {
let label = tsEntry.name let label = tsEntry.name
let sortText = tsEntry.sortText let sortText = tsEntry.sortText
let preselect = false
let detail: string
if (tsEntry.isRecommended) { if (tsEntry.isRecommended) {
// Make sure isRecommended property always comes first preselect = true
// https://github.com/Microsoft/vscode/issues/40325 }
sortText = '\0' + sortText if (tsEntry.source) {
} else if (tsEntry.source) { // De-prioritze auto-imports https://github.com/Microsoft/vscode/issues/40311
// De-prioritze auto-imports
// https://github.com/Microsoft/vscode/issues/40311
sortText = '\uffff' + sortText sortText = '\uffff' + sortText
} else { } else {
sortText = tsEntry.sortText sortText = tsEntry.sortText
} }
let kind = convertKind(tsEntry.kind) let kind = convertKind(tsEntry.kind)
let insertTextFormat = ( let insertTextFormat = (
useCodeSnippetsOnMethodSuggest && context.enableCallCompletions &&
(kind === CompletionItemKind.Function || (kind === CompletionItemKind.Function ||
kind === CompletionItemKind.Method) kind === CompletionItemKind.Method)
) ? InsertTextFormat.Snippet : InsertTextFormat.PlainText ) ? InsertTextFormat.Snippet : InsertTextFormat.PlainText
let insertText = tsEntry.insertText let insertText = tsEntry.insertText
let document = workspace.getDocument(uri) let commitCharacters = getCommitCharacters(tsEntry, context)
let preText = document.getline(position.line).slice(0, position.character)
const isInValidCommitCharacterContext = preText.match(/(^|[a-z_$\(\)\[\]\{\}]|[^.]\.)\s*$/ig) !== null
let commitCharacters = getCommitCharacters(tsEntry, { isNewIdentifierLocation, isInValidCommitCharacterContext, useCodeSnippetsOnMethodSuggest })
let optional = tsEntry.kindModifiers && tsEntry.kindModifiers.match(/\boptional\b/)
let textEdit: TextEdit | null = null let textEdit: TextEdit | null = null
if (tsEntry.replacementSpan) { if (tsEntry.replacementSpan) {
let { start, end } = 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 { return {
label, label,
insertText, insertText,
textEdit, textEdit,
kind, kind,
preselect,
insertTextFormat, insertTextFormat,
sortText, sortText,
commitCharacters, commitCharacters,
detail,
data: { data: {
uri, uri,
optional,
position, position,
source: tsEntry.source || '' source: tsEntry.source || ''
} }
@ -97,7 +122,7 @@ function convertKind(kind: string): CompletionItemKind {
return CompletionItemKind.Field return CompletionItemKind.Field
case PConst.Kind.function: case PConst.Kind.function:
return CompletionItemKind.Function return CompletionItemKind.Function
case PConst.Kind.memberFunction: case PConst.Kind.method:
case PConst.Kind.constructSignature: case PConst.Kind.constructSignature:
case PConst.Kind.callSignature: case PConst.Kind.callSignature:
case PConst.Kind.indexSignature: case PConst.Kind.indexSignature:
@ -121,8 +146,8 @@ function convertKind(kind: string): CompletionItemKind {
return CompletionItemKind.Variable return CompletionItemKind.Variable
} }
function getCommitCharacters(tsEntry: Proto.CompletionEntry, settings: CommitCharactersSettings): string[] | undefined { function getCommitCharacters(tsEntry: Proto.CompletionEntry, context: CompletionContext): string[] | undefined {
if (settings.isNewIdentifierLocation || !settings.isInValidCommitCharacterContext) { if (context.isNewIdentifierLocation || !context.isInValidCommitCharacterContext) {
return undefined return undefined
} }
const commitCharacters: string[] = [] const commitCharacters: string[] = []
@ -145,10 +170,11 @@ function getCommitCharacters(tsEntry: Proto.CompletionEntry, settings: CommitCha
case PConst.Kind.memberVariable: case PConst.Kind.memberVariable:
case PConst.Kind.class: case PConst.Kind.class:
case PConst.Kind.function: case PConst.Kind.function:
case PConst.Kind.memberFunction: case PConst.Kind.method:
case PConst.Kind.keyword: case PConst.Kind.keyword:
case PConst.Kind.parameter:
commitCharacters.push('.', ',', ';') commitCharacters.push('.', ',', ';')
if (settings.useCodeSnippetsOnMethodSuggest) { if (context.enableCallCompletions) {
commitCharacters.push('(') commitCharacters.push('(')
} }
break break