diff --git a/package.json b/package.json index 5cb415a..0ff31ae 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "coc-tsserver", - "version": "1.8.1", + "version": "1.8.5", "description": "tsserver extension for coc.nvim", "main": "lib/index.js", "publisher": "chemzqm", @@ -265,6 +265,11 @@ "default": true, "description": "Show unused variable hint." }, + "typescript.showDeprecated": { + "type": "boolean", + "default": true, + "description": "Show deprecated variable hint." + }, "typescript.updateImportsOnFileMove.enable": { "type": "boolean", "default": true, @@ -473,6 +478,11 @@ "default": true, "description": "Show unused variable hint." }, + "javascript.showDeprecated": { + "type": "boolean", + "default": true, + "description": "Show deprecated variable hint." + }, "javascript.updateImportsOnFileMove.enable": { "type": "boolean", "default": true @@ -675,13 +685,13 @@ "license": "MIT", "devDependencies": { "@types/node": "^10.12.0", - "coc.nvim": "^0.0.80", + "coc.nvim": "^0.0.81-next.5", "esbuild": "^0.8.29", "semver": "^7.3.2", "vscode-languageserver-protocol": "^3.16.0", "which": "^2.0.2" }, "dependencies": { - "typescript": "^4.3.2" + "typescript": "^4.3.5" } } diff --git a/src/server/features/callHierarchy.ts b/src/server/features/callHierarchy.ts index 90f449e..af6338a 100644 --- a/src/server/features/callHierarchy.ts +++ b/src/server/features/callHierarchy.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CallHierarchyProvider, TextDocument, Uri } from 'coc.nvim' +import { CallHierarchyProvider, TextDocument, Uri, workspace } from 'coc.nvim' import path from "path" import { CallHierarchyIncomingCall, CallHierarchyItem, CallHierarchyOutgoingCall, CancellationToken, Position, SymbolTag } from 'vscode-languageserver-protocol' import type * as Proto from '../protocol' @@ -80,9 +80,7 @@ function parseKindModifier(kindModifiers: string): Set { function fromProtocolCallHierarchyItem(item: Proto.CallHierarchyItem): CallHierarchyItem { const useFileName = isSourceFileItem(item) const name = useFileName ? path.basename(item.file) : item.name - // TODO - // const detail = useFileName ? workspace.asRelativePath(path.dirname(item.file)) : item.containerName ?? '' - const detail = item.containerName || '' + const detail = useFileName ? path.relative(workspace.cwd, path.dirname(item.file)) : item.containerName ?? '' const result: CallHierarchyItem = { name, detail, @@ -93,7 +91,7 @@ function fromProtocolCallHierarchyItem(item: Proto.CallHierarchyItem): CallHiera } const kindModifiers = item.kindModifiers ? parseKindModifier(item.kindModifiers) : undefined - if (kindModifiers?.has(PConst.KindModifiers.depreacted)) { + if (kindModifiers?.has(PConst.KindModifiers.deprecated)) { result.tags = [SymbolTag.Deprecated] } return result diff --git a/src/server/features/completionItemProvider.ts b/src/server/features/completionItemProvider.ts index 49060fd..7f2ad21 100644 --- a/src/server/features/completionItemProvider.ts +++ b/src/server/features/completionItemProvider.ts @@ -1,9 +1,9 @@ -import { commands, CompletionItemProvider, TextDocument, window, workspace } from 'coc.nvim' +import { commands, CompletionItemProvider, TextDocument, CompletionList, CompletionItem, window, workspace } from 'coc.nvim' /*--------------------------------------------------------------------------------------------- * 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, CompletionList, InsertTextFormat, MarkupContent, MarkupKind, Position, Range, TextEdit } from 'vscode-languageserver-protocol' +import { CancellationToken, Command, CompletionContext, InsertTextFormat, MarkupContent, MarkupKind, Position, Range, TextEdit } from 'vscode-languageserver-protocol' import Proto from '../protocol' import * as PConst from '../protocol.const' import { ITypeScriptServiceClient, ServerResponse } from '../typescriptService' @@ -344,7 +344,7 @@ export default class TypeScriptCompletionItemProvider implements CompletionItemP if (!this.completeOption.importStatementSuggestions || !this.client.apiVersion.lt(API.v430)) { return false } - return pre === 'import '; + return pre === 'import ' } return true diff --git a/src/server/features/definitionProvider.ts b/src/server/features/definitionProvider.ts index 3959bcf..ea57402 100644 --- a/src/server/features/definitionProvider.ts +++ b/src/server/features/definitionProvider.ts @@ -3,10 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { TextDocument } from 'coc.nvim' -import { DefinitionProvider, ImplementationProvider, TypeDefinitionProvider } from 'coc.nvim' -import { CancellationToken, Definition, Location, Position } from 'vscode-languageserver-protocol' +import { DefinitionProvider, CancellationToken, Definition, Location, Position, DefinitionLink, ImplementationProvider, TypeDefinitionProvider } from 'coc.nvim' import * as Proto from '../protocol' import { ITypeScriptServiceClient } from '../typescriptService' +import API from '../utils/api' import * as typeConverters from '../utils/typeConverters' export default class TypeScriptDefinitionProvider implements DefinitionProvider, TypeDefinitionProvider, ImplementationProvider { @@ -41,12 +41,44 @@ export default class TypeScriptDefinitionProvider implements DefinitionProvider, } } - public provideDefinition( + public async provideDefinition( document: TextDocument, position: Position, token: CancellationToken - ): Promise { - return this.getSymbolLocations('definition', document, position, token) + ): Promise { + if (this.client.apiVersion.gte(API.v270)) { + const filepath = this.client.toOpenedFilePath(document.uri) + if (!filepath) { + return undefined + } + + const args = typeConverters.Position.toFileLocationRequestArgs(filepath, position) + const response = await this.client.execute('definitionAndBoundSpan', args, token) + if (response.type !== 'response' || !response.body) { + return undefined + } + + const span = response.body.textSpan ? typeConverters.Range.fromTextSpan(response.body.textSpan) : undefined + return response.body.definitions + .map((location): DefinitionLink => { + const target = typeConverters.Location.fromTextSpan(this.client.toResource(location.file), location) + if (location.contextStart && location.contextEnd) { + return { + originSelectionRange: span, + targetRange: typeConverters.Range.fromLocations(location.contextStart, location.contextEnd), + targetUri: target.uri, + targetSelectionRange: target.range, + } + } + return { + originSelectionRange: span, + targetRange: target.range, + targetUri: target.uri, + targetSelectionRange: target.range, + } + }) + } + return await this.getSymbolLocations('definition', document, position, token) } public provideTypeDefinition( diff --git a/src/server/features/diagnostics.ts b/src/server/features/diagnostics.ts index 2b8810c..28f6f0e 100644 --- a/src/server/features/diagnostics.ts +++ b/src/server/features/diagnostics.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { DiagnosticCollection, languages, workspace } from 'coc.nvim' -import { Diagnostic } from 'vscode-languageserver-protocol' +import { Diagnostic, DiagnosticTag } from 'vscode-languageserver-protocol' import { ResourceMap } from './resourceMap' export class DiagnosticSet { @@ -146,8 +146,7 @@ export class DiagnosticsManager { .get(uri) .filter(x => { if (!enabled) { - // Still show unused - return x.code == 6133 + return x.tags && (x.tags.includes(DiagnosticTag.Unnecessary) || x.tags.includes(DiagnosticTag.Deprecated)) } return x.code !== 80001 // disable annoying CommonJS module warning }) diff --git a/src/server/features/directiveCommentCompletions.ts b/src/server/features/directiveCommentCompletions.ts index d03fe80..bc49cc0 100644 --- a/src/server/features/directiveCommentCompletions.ts +++ b/src/server/features/directiveCommentCompletions.ts @@ -2,8 +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, CompletionContext, CompletionItem, CompletionItemKind, CompletionList, Position, Range } from 'vscode-languageserver-protocol' -import { TextDocument } from 'coc.nvim' +import { CancellationToken, CompletionContext, CompletionItem, CompletionItemKind, CompletionList, Position, Range, TextDocument } from 'coc.nvim' import { workspace } from 'coc.nvim' import { ITypeScriptServiceClient } from '../typescriptService' import API from '../utils/api' @@ -37,7 +36,7 @@ const tsDirectives390: Directive[] = [ ] export default class DirectiveCommentCompletionProvider { - constructor(private readonly client: ITypeScriptServiceClient) { } + constructor(private readonly client: ITypeScriptServiceClient) {} public provideCompletionItems( document: TextDocument, @@ -62,7 +61,7 @@ export default class DirectiveCommentCompletionProvider { ? tsDirectives390 : tsDirectives let items = directives.map(directive => { - const item = CompletionItem.create(directive.value) + const item: CompletionItem = { label: directive.value } item.kind = CompletionItemKind.Snippet item.detail = directive.description item.textEdit = { diff --git a/src/server/features/documentSymbol.ts b/src/server/features/documentSymbol.ts index 6bb4110..98a4586 100644 --- a/src/server/features/documentSymbol.ts +++ b/src/server/features/documentSymbol.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { TextDocument } from 'coc.nvim' import { DocumentSymbolProvider } from 'coc.nvim' -import { CancellationToken, DocumentSymbol, Range, SymbolKind } from 'vscode-languageserver-protocol' +import { CancellationToken, DocumentSymbol, Range, SymbolKind, SymbolTag } from 'vscode-languageserver-protocol' import * as Proto from '../protocol' import * as PConst from '../protocol.const' import { ITypeScriptServiceClient } from '../typescriptService' @@ -82,20 +82,15 @@ export default class TypeScriptDocumentSymbolProvider implements DocumentSymbolP } private static convertNavTree( - bucket: DocumentSymbol[], + output: DocumentSymbol[], item: Proto.NavigationTree, ): boolean { let shouldInclude = TypeScriptDocumentSymbolProvider.shouldInclueEntry(item) const children = new Set(item.childItems || []) for (const span of item.spans) { const range = typeConverters.Range.fromTextSpan(span) - const symbolInfo = DocumentSymbol.create( - item.text, - '', - getSymbolKind(item.kind), - range, - range) - symbolInfo.children = children.size > 0 ? [] : null + const symbolInfo = TypeScriptDocumentSymbolProvider.convertSymbol(item, range) + if (children.size) symbolInfo.children = [] for (const child of children) { if (child.spans.some(span => !!containsRange(range, typeConverters.Range.fromTextSpan(span)))) { @@ -106,13 +101,33 @@ export default class TypeScriptDocumentSymbolProvider implements DocumentSymbolP } if (shouldInclude) { - bucket.push(symbolInfo) + output.push(symbolInfo) } } - return shouldInclude } + private static convertSymbol(item: Proto.NavigationTree, range: Range): DocumentSymbol { + const selectionRange = item.nameSpan ? typeConverters.Range.fromTextSpan(item.nameSpan) : range + let label = item.text + switch (item.kind) { + case PConst.Kind.memberGetAccessor: label = `(get) ${label}`; break + case PConst.Kind.memberSetAccessor: label = `(set) ${label}`; break + } + const symbolInfo = DocumentSymbol.create( + label, + '', + getSymbolKind(item.kind), + range, + containsRange(range, selectionRange) ? selectionRange : range) + + const kindModifiers = parseKindModifier(item.kindModifiers) + if (kindModifiers.has(PConst.KindModifiers.deprecated)) { + symbolInfo.tags = [SymbolTag.Deprecated] + } + return symbolInfo + } + private static shouldInclueEntry( item: Proto.NavigationTree | Proto.NavigationBarItem ): boolean { @@ -142,3 +157,7 @@ function containsRange(range: Range, otherRange: Range): boolean { } return true } + +function parseKindModifier(kindModifiers: string): Set { + return new Set(kindModifiers.split(/,|\s+/g)) +} diff --git a/src/server/features/tagClosing.ts b/src/server/features/tagClosing.ts index b55c127..bb53810 100644 --- a/src/server/features/tagClosing.ts +++ b/src/server/features/tagClosing.ts @@ -2,8 +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 { CancellationTokenSource, Disposable, disposeAll, Position, Range, snippetManager, window, workspace } from 'coc.nvim' -import { TextDocumentContentChangeEvent } from 'vscode-languageserver-protocol' +import { CancellationTokenSource, Disposable, disposeAll, Position, Range, snippetManager, events, workspace, InsertChange } from 'coc.nvim' import * as Proto from '../protocol' import { ITypeScriptServiceClient } from '../typescriptService' import API from '../utils/api' @@ -19,49 +18,82 @@ export default class TagClosing implements Disposable { } private _disposables: Disposable[] = [] - private _enabled: boolean = false private _disposed = false private _timeout: NodeJS.Timer | undefined = undefined private _cancel: CancellationTokenSource | undefined = undefined + private lastInsert: string constructor( private readonly client: ITypeScriptServiceClient, private readonly descriptionLanguageId: string ) { - workspace.onDidChangeTextDocument( - (event) => - this.onDidChangeTextDocument( - event.textDocument, - event.contentChanges - ), - null, - this._disposables - ) - - this.updateEnabledState() - - workspace.registerAutocmd({ - event: ['BufEnter'], - request: false, - callback: () => this.updateEnabledState(), - }) + events.on('InsertCharPre', character => { + this.lastInsert = character + }, null, this._disposables) + events.on('TextChangedI', this.onChange, this, this._disposables) + events.on('TextChangedP', this.onChange, this, this._disposables) } - async updateEnabledState(): Promise { - this._enabled = false - const doc = await workspace.document - if (!doc) { - return + private async onChange(bufnr: number, change: InsertChange): Promise { + let doc = workspace.getDocument((bufnr)) + if (!doc || !doc.attached) return + let enabled = this.isEnabled(doc.filetype, doc.uri) + if (!enabled) return + let { pre, changedtick, lnum } = change + if (!pre.endsWith('/') && !pre.endsWith('>')) return + if (!pre.endsWith(this.lastInsert)) return + if (pre.length > 1 && pre[pre.length - 2] == '>') return + const filepath = this.client.toOpenedFilePath(doc.uri) + if (!filepath) return + if (this._timeout) { + clearTimeout(this._timeout) } - const document = doc.textDocument - const configLang = TagClosing._configurationLanguages[document.languageId] + if (this._cancel) { + this._cancel.cancel() + this._cancel.dispose() + this._cancel = undefined + } + await (doc as any).patchChange() + this._timeout = setTimeout(async () => { + this._timeout = undefined + if (this._disposed) return + if (doc.changedtick > changedtick) return + const position = Position.create(lnum - 1, pre.length) + const args: Proto.JsxClosingTagRequestArgs = typeConverters.Position.toFileLocationRequestArgs( + filepath, + position + ) + this._cancel = new CancellationTokenSource() + const response = await this.client.execute( + 'jsxClosingTag', + args, + this._cancel.token + ) + if (response.type !== 'response' || !response.body) { + return + } + if (this._disposed) return + + const insertion = response.body + if (doc.changedtick === changedtick) { + snippetManager.insertSnippet( + this.getTagSnippet(insertion).value, + false, + Range.create(position, position) + ) + } + }, 50) + } + + private isEnabled(languageId: string, uri: string): boolean { + const configLang = TagClosing._configurationLanguages[languageId] if (!configLang || configLang !== this.descriptionLanguageId) { - return + return false } - if (!workspace.getConfiguration(undefined, document.uri).get(`${configLang}.autoClosingTags`)) { - return + if (!workspace.getConfiguration(undefined, uri).get(`${configLang}.autoClosingTags`)) { + return false } - this._enabled = true + return true } public dispose() { @@ -82,120 +114,10 @@ export default class TagClosing implements Disposable { this._disposables = [] } - private async onDidChangeTextDocument( - documentEvent: { - uri: string, - version: number, - }, - changes: readonly TextDocumentContentChangeEvent[] - ) { - if (!this._enabled) { - return - } - const document = await workspace.document - if (!document) { - return - } - const activeDocument = document.textDocument - if (activeDocument.uri !== documentEvent.uri || changes.length === 0) { - return - } - const filepath = this.client.toOpenedFilePath(documentEvent.uri) - if (!filepath) { - return - } - - if (typeof this._timeout !== 'undefined') { - clearTimeout(this._timeout) - } - - if (this._cancel) { - this._cancel.cancel() - this._cancel.dispose() - this._cancel = undefined - } - - const lastChange = changes[changes.length - 1] - if (!Range.is(lastChange['range']) || !lastChange.text) { - return - } - - const lastCharacter = lastChange.text[lastChange.text.length - 1] - if (lastCharacter !== '>' && lastCharacter !== '/') { - return - } - - const version = documentEvent.version - - const rangeStart = lastChange['range'].start - const priorCharacter = - lastChange['range'].start.character > 0 - ? activeDocument.getText( - Range.create( - Position.create(rangeStart.line, rangeStart.character - 1), - rangeStart - ) - ) - : '' - if (priorCharacter === '>') { - return - } - - this._timeout = setTimeout(async () => { - this._timeout = undefined - - if (this._disposed) { - return - } - - const addedLines = lastChange.text.split(/\r\n|\n/g) - const position = - addedLines.length <= 1 - ? Position.create( - rangeStart.line, - rangeStart.character + lastChange.text.length - ) - : Position.create( - rangeStart.line + addedLines.length - 1, - addedLines[addedLines.length - 1].length - ) - - const args: Proto.JsxClosingTagRequestArgs = typeConverters.Position.toFileLocationRequestArgs( - filepath, - position - ) - this._cancel = new CancellationTokenSource() - const response = await this.client.execute( - 'jsxClosingTag', - args, - this._cancel.token - ) - if (response.type !== 'response' || !response.body) { - return - } - - if (this._disposed) { - return - } - - const insertion = response.body; - if ( - documentEvent.uri === activeDocument.uri && - activeDocument.version === version - ) { - snippetManager.insertSnippet( - this.getTagSnippet(insertion).value, - false, - Range.create(position, position) - ) - } - }, 100); - } - private getTagSnippet(closingTag: Proto.TextInsertion): SnippetString { - const snippet = new SnippetString(); - snippet.appendPlaceholder('', 0); - snippet.appendText(closingTag.newText); - return snippet; + const snippet = new SnippetString() + snippet.appendPlaceholder('', 0) + snippet.appendText(closingTag.newText) + return snippet } } diff --git a/src/server/features/workspaceSymbols.ts b/src/server/features/workspaceSymbols.ts index 679b8dc..15342fa 100644 --- a/src/server/features/workspaceSymbols.ts +++ b/src/server/features/workspaceSymbols.ts @@ -4,12 +4,17 @@ *--------------------------------------------------------------------------------------------*/ import { workspace } from 'coc.nvim' import { WorkspaceSymbolProvider } from 'coc.nvim' -import { CancellationToken, Range, SymbolInformation, SymbolKind } from 'vscode-languageserver-protocol' +import { CancellationToken, Range, SymbolInformation, SymbolKind, SymbolTag } from 'vscode-languageserver-protocol' import * as Proto from '../protocol' +import * as PConst from '../protocol.const' import { ITypeScriptServiceClient } from '../typescriptService' import API from '../utils/api' import * as typeConverters from '../utils/typeConverters' +function parseKindModifier(kindModifiers: string): Set { + return new Set(kindModifiers.split(/,|\s+/g)) +} + function getSymbolKind(item: Proto.NavtoItem): SymbolKind { switch (item.kind) { case 'method': @@ -75,7 +80,10 @@ export default class TypeScriptWorkspaceSymbolProvider implements WorkspaceSymbo getSymbolKind(item), range, this.client.toResource(item.file)) - + const kindModifiers = item.kindModifiers ? parseKindModifier(item.kindModifiers) : undefined + if (kindModifiers?.has(PConst.KindModifiers.deprecated)) { + symbolInfo.tags = [SymbolTag.Deprecated] + } result.push(symbolInfo) } return result diff --git a/src/server/languageProvider.ts b/src/server/languageProvider.ts index 293775d..efdb855 100644 --- a/src/server/languageProvider.ts +++ b/src/server/languageProvider.ts @@ -44,7 +44,7 @@ export default class LanguageProvider { public client: TypeScriptServiceClient, private readonly fileConfigurationManager: FileConfigurationManager, private description: LanguageDescription, - private typingsStatus: TypingsStatus + typingsStatus: TypingsStatus ) { workspace.onDidChangeConfiguration(this.configurationChanged, this, this.disposables) this.configurationChanged() @@ -86,7 +86,7 @@ export default class LanguageProvider { if (this.client.apiVersion.gte(API.v230)) { this._register(languages.registerCompletionItemProvider( `${this.description.id}-directive`, - 'TSC', languageIds, new DirectiveCommentCompletionProvider(client,), ['@'] + 'TSC', languageIds, new DirectiveCommentCompletionProvider(client), ['@'] )) } @@ -179,17 +179,22 @@ export default class LanguageProvider { public diagnosticsReceived( diagnosticsKind: DiagnosticKind, file: Uri, - diagnostics: (Diagnostic & { reportUnnecessary: any })[] + diagnostics: (Diagnostic & { reportUnnecessary: any, reportDeprecated: any })[] ): void { const config = workspace.getConfiguration(this.id, file.toString()) const reportUnnecessary = config.get('showUnused', true) + const reportDeprecated = config.get('showDeprecated', true) this.client.diagnosticsManager.diagnosticsReceived(diagnosticsKind, file.toString(), diagnostics.filter(diag => { if (!reportUnnecessary) { - diag.tags = undefined if (diag.reportUnnecessary && diag.severity === DiagnosticSeverity.Information) { return false } } + if (!reportDeprecated) { + if (diag.reportDeprecated && diag.severity === DiagnosticSeverity.Hint) { + return false + } + } return true })) } diff --git a/src/server/protocol.const.ts b/src/server/protocol.const.ts index 546f136..47da389 100644 --- a/src/server/protocol.const.ts +++ b/src/server/protocol.const.ts @@ -44,7 +44,7 @@ export class DiagnosticCategory { export class KindModifiers { public static readonly optional = 'optional' - public static readonly depreacted = 'deprecated' + public static readonly deprecated = 'deprecated' public static readonly color = 'color' public static readonly dtsFile = '.d.ts' @@ -60,7 +60,7 @@ export class KindModifiers { KindModifiers.tsxFile, KindModifiers.jsFile, KindModifiers.jsxFile, - KindModifiers.jsonFile, + KindModifiers.jsonFile ] } @@ -72,3 +72,19 @@ export class DisplayPartKind { public static readonly punctuation = 'punctuation' public static readonly text = 'text' } + +export enum EventName { + syntaxDiag = 'syntaxDiag', + semanticDiag = 'semanticDiag', + suggestionDiag = 'suggestionDiag', + configFileDiag = 'configFileDiag', + telemetry = 'telemetry', + projectLanguageServiceState = 'projectLanguageServiceState', + projectsUpdatedInBackground = 'projectsUpdatedInBackground', + beginInstallTypes = 'beginInstallTypes', + endInstallTypes = 'endInstallTypes', + typesInstallerInitializationFailed = 'typesInstallerInitializationFailed', + surveyReady = 'surveyReady', + projectLoadingStart = 'projectLoadingStart', + projectLoadingFinish = 'projectLoadingFinish' +} diff --git a/src/server/typescriptServiceClientHost.ts b/src/server/typescriptServiceClientHost.ts index 5f1479a..dea0311 100644 --- a/src/server/typescriptServiceClientHost.ts +++ b/src/server/typescriptServiceClientHost.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { disposeAll, languages, TextDocument, Uri, workspace } from 'coc.nvim' -import { CancellationToken, Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, Disposable, Position, Range } from 'vscode-languageserver-protocol' +import { CancellationToken, Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, DiagnosticTag, Disposable, Position, Range } from 'vscode-languageserver-protocol' import { flatten } from '../utils/arrays' import { PluginManager } from '../utils/plugins' import { DiagnosticKind } from './features/diagnostics' @@ -217,11 +217,11 @@ export default class TypeScriptServiceClientHost implements Disposable { } } - private createMarkerDatas(diagnostics: Proto.Diagnostic[]): (Diagnostic & { reportUnnecessary: any })[] { + private createMarkerDatas(diagnostics: Proto.Diagnostic[]): (Diagnostic & { reportUnnecessary: any, reportDeprecated: any })[] { return diagnostics.map(tsDiag => this.tsDiagnosticToLspDiagnostic(tsDiag)) } - private tsDiagnosticToLspDiagnostic(diagnostic: Proto.Diagnostic): (Diagnostic & { reportUnnecessary: any }) { + private tsDiagnosticToLspDiagnostic(diagnostic: Proto.Diagnostic): (Diagnostic & { reportUnnecessary: any, reportDeprecated: any }) { const { start, end, text } = diagnostic const range = { start: typeConverters.Position.fromLocation(start), @@ -237,11 +237,22 @@ export default class TypeScriptServiceClientHost implements Disposable { } }) } + let tags: DiagnosticTag[] | undefined = [] + if (diagnostic.reportsUnnecessary) { + tags.push(DiagnosticTag.Unnecessary) + } + if (diagnostic.reportsDeprecated) { + tags.push(DiagnosticTag.Deprecated) + } + tags = tags.length ? tags : undefined + return { range, + tags, message: text, code: diagnostic.code ? diagnostic.code : null, severity: this.getDiagnosticSeverity(diagnostic), + reportDeprecated: diagnostic.reportsDeprecated, reportUnnecessary: diagnostic.reportsUnnecessary, source: diagnostic.source || 'tsserver', relatedInformation diff --git a/src/server/utils/completionItem.ts b/src/server/utils/completionItem.ts index d6b4cbb..1ccfec2 100644 --- a/src/server/utils/completionItem.ts +++ b/src/server/utils/completionItem.ts @@ -2,7 +2,8 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Range, CompletionItem, CompletionItemKind, InsertTextFormat, Position, TextEdit } from 'vscode-languageserver-protocol' +import { Range, CompletionItem, CompletionItemKind, InsertTextFormat, Position, TextEdit } from 'coc.nvim' +import { CompletionItemTag } from 'vscode-languageserver-protocol' import * as Proto from '../protocol' import * as PConst from '../protocol.const' @@ -52,6 +53,7 @@ export function convertCompletionEntry( let insertText = tsEntry.insertText let commitCharacters = getCommitCharacters(tsEntry, context) + let tags: CompletionItemTag[] if (tsEntry.isImportStatementCompletion) { insertText = label @@ -75,6 +77,10 @@ export function convertCompletionEntry( label += '?' } + if (kindModifiers.has(PConst.KindModifiers.deprecated)) { + tags = [CompletionItemTag.Deprecated] + } + if (kindModifiers.has(PConst.KindModifiers.color)) { kind = CompletionItemKind.Color } @@ -97,6 +103,7 @@ export function convertCompletionEntry( insertText, textEdit, kind, + tags, preselect, insertTextFormat, sortText, diff --git a/src/server/utils/previewer.ts b/src/server/utils/previewer.ts index 07d53b7..ccae4f5 100644 --- a/src/server/utils/previewer.ts +++ b/src/server/utils/previewer.ts @@ -5,37 +5,83 @@ import { MarkupContent, MarkupKind } from 'vscode-languageserver-protocol' import * as Proto from '../protocol' +import { Uri } from 'coc.nvim' + +function toResource(filepath: string): Uri { + return Uri.file(filepath) +} + +function replaceLinks(text: string): string { + return text + // Http(s) links + .replace(/\{@(link|linkplain|linkcode) (https?:\/\/[^ |}]+?)(?:[| ]([^{}\n]+?))?\}/gi, (_, tag: string, link: string, text?: string) => { + switch (tag) { + case 'linkcode': + return `[\`${text ? text.trim() : link}\`](${link})` + + default: + return `[${text ? text.trim() : link}](${link})` + } + }) +} + +function processInlineTags(text: string): string { + return replaceLinks(text) +} function getTagBodyText(tag: Proto.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```' + // Convert to markdown code block if it is not already one + function makeCodeblock(text: string): string { + if (text.match(/^\s*[~`]{3}/g)) { + return text + } + return '```\n' + text + '\n```' } - return tag.text + const text = convertLinkTags(tag.text) + switch (tag.name) { + case 'example': + // check for caption tags, fix for #79704 + const captionTagMatches = text.match(/(.*?)<\/caption>\s*(\r\n|\n)/) + if (captionTagMatches && captionTagMatches.index === 0) { + return captionTagMatches[1] + '\n\n' + makeCodeblock(text.substr(captionTagMatches[0].length)) + } else { + return makeCodeblock(text) + } + case 'author': + // fix obsucated email address, #80898 + const emailMatch = text.match(/(.+)\s<([-.\w]+@[-.\w]+)>/) + + if (emailMatch === null) { + return text + } else { + return `${emailMatch[1]} ${emailMatch[2]}` + } + case 'default': + return makeCodeblock(text) + } + + return processInlineTags(text) } function getTagDocumentation(tag: Proto.JSDocTagInfo): string | undefined { switch (tag.name) { + case 'augments': + case 'extends': case 'param': - const body = (tag.text || '').split(/^([\w\.]+)\s*/) - if (body && body.length === 3) { + case 'template': + const body = (convertLinkTags(tag.text)).split(/^(\S+)\s*-?\s*/) + if (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}`) + return label + (doc.match(/\r\n|\n/g) ? ' \n' + processInlineTags(doc) : ` — ${processInlineTags(doc)}`) } } @@ -45,7 +91,7 @@ function getTagDocumentation(tag: Proto.JSDocTagInfo): string | undefined { if (!text) { return label } - return label + (text.match(/\r\n|\n/g) ? '\n' + text : ` — ${text}`) + return label + (text.match(/\r\n|\n/g) ? ' \n' + text : ` — ${text}`) } export function plain(parts: Proto.SymbolDisplayPart[]): string { @@ -71,3 +117,73 @@ export function markdownDocumentation( value: out } } + +/** + * Convert `@link` inline tags to markdown links + */ +function convertLinkTags( + parts: readonly Proto.SymbolDisplayPart[] | string | undefined +): string { + if (!parts) { + return '' + } + + if (typeof parts === 'string') { + return parts + } + + const out: string[] = [] + let currentLink: { name?: string, target?: Proto.FileSpan, text?: string } | undefined + for (const part of parts) { + switch (part.kind) { + case 'link': + if (currentLink) { + const text = currentLink.text ?? currentLink.name + if (currentLink.target) { + const link = toResource(currentLink.target.file) + .with({ + fragment: `L${currentLink.target.start.line},${currentLink.target.start.offset}` + }) + + out.push(`[${text}](${link.toString()})`) + } else { + if (text) { + if (/^https?:/.test(text)) { + const parts = text.split(' ') + if (parts.length === 1) { + out.push(parts[0]) + } else if (parts.length > 1) { + out.push(`[${parts.slice(1).join(' ')}](${parts[0]})`) + } + } else { + out.push(text) + } + } + } + currentLink = undefined + } else { + currentLink = {} + } + break + + case 'linkName': + if (currentLink) { + currentLink.name = part.text + // TODO: remove cast once we pick up TS 4.3 + currentLink.target = (part as any as Proto.JSDocLinkDisplayPart).target + } + break + + case 'linkText': + if (currentLink) { + currentLink.text = part.text + } + break + + default: + out.push(part.text) + break + } + } + return processInlineTags(out.join('')) +} diff --git a/src/server/utils/typeConverters.ts b/src/server/utils/typeConverters.ts index 0703ce1..ace1aa1 100644 --- a/src/server/utils/typeConverters.ts +++ b/src/server/utils/typeConverters.ts @@ -23,6 +23,11 @@ export namespace Range { } } } + export const fromLocations = (start: Proto.Location, end: Proto.Location): language.Range => + language.Range.create( + Math.max(0, start.line - 1), Math.max(start.offset - 1, 0), + Math.max(0, end.line - 1), Math.max(0, end.offset - 1)) + export const toFormattingRequestArgs = (file: string, range: language.Range): Proto.FormatRequestArgs => ({ file, diff --git a/yarn.lock b/yarn.lock index 6553c0b..76a495a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7,10 +7,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.44.tgz#3945e6b702cb6403f22b779c8ea9e5c3f44ead40" integrity sha512-vHPAyBX1ffLcy4fQHmDyIUMUb42gHZjPHU66nhvbMzAWJqHnySGZ6STwN3rwrnSd1FHB0DI/RWgGELgKSYRDmw== -coc.nvim@^0.0.80: - version "0.0.80" - resolved "https://registry.yarnpkg.com/coc.nvim/-/coc.nvim-0.0.80.tgz#785145c382660db03f517f9b497900d95cbd0e4f" - integrity sha512-/3vTcnofoAYMrdENrlQmADTzfXX4+PZ0fiM10a39UA37dTR2dpIGi9O469kcIksuunLjToqWG8S45AGx/9wV7g== +coc.nvim@^0.0.81-next.5: + version "0.0.81-next.5" + resolved "https://registry.yarnpkg.com/coc.nvim/-/coc.nvim-0.0.81-next.5.tgz#b09bda5a2d527f7cd7bd786d4dee958160285e2b" + integrity sha512-NUTEWmjm9uKGBD8FNsj55PH1Xn4hD5uDCs7EdMoBpXWehq6/P1UoDZQfWXQv9HTq4zF/jH3g7KtZtl94VTlW4A== esbuild@^0.8.29: version "0.8.29" @@ -27,10 +27,10 @@ semver@^7.3.2: resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== -typescript@^4.3.2: - version "4.3.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.2.tgz#399ab18aac45802d6f2498de5054fcbbe716a805" - integrity sha512-zZ4hShnmnoVnAHpVHWpTcxdv7dWP60S2FsydQLV8V5PbS3FifjWFFRiHSWpDJahly88PRyV5teTSLoq4eG7mKw== +typescript@^4.3.5: + version "4.3.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.5.tgz#4d1c37cc16e893973c45a06886b7113234f119f4" + integrity sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA== vscode-jsonrpc@6.0.0: version "6.0.0"