commit 54d03a04c14c45210c2a0f6a958b36659ed766fb Author: chemzqm <chemzqm@gmail.com> Date: Fri Sep 7 20:40:51 2018 +0800 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a65b417 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +lib diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..9db86d7 --- /dev/null +++ b/.npmignore @@ -0,0 +1,4 @@ +src +tsconfig.json +tslint.json +*.map diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..3889bde --- /dev/null +++ b/Readme.md @@ -0,0 +1,50 @@ +# coc-tsserver + +Tsserver language server extension for [coc.nvim](https://github.com/neoclide/coc.nvim). + +Most code from `typescript-language-features` extension which bundled with VSCode. + +## Install + +In your vim/neovim, run command: + +``` +:CocInstall coc-tsserver +``` + +## Features + +Almost same as VSCode. + +* Support javascript & typescript and jsx/tsx. +* Install typings automatically. +* Commands to work with tsserver. +* Code completion support. +* Go to definition. +* Code validation. +* Document highlight. +* Document symbols of current buffer. +* Folding and folding range of current buffer. +* Format current buffer, range format and format on type. +* Hover for documentation. +* Implementations codeLens and references codeLens. +* Organize imports command. +* Quickfix using code actions. +* Code refactor using code actions. +* Find references. +* Signature help. +* Rename symbols support. +* Rename imports on file rename. +* Search for workspace symbols. + +## Configuration options + +* `tsserver.enable` set to `false` to disable tsserver language server. +* `tsserver.trace.server` trace LSP traffic in output channel. + +And many more, which are same as VSCode, trigger completion in your +`coc-settings.json` to get full list. + +## License + +MIT diff --git a/package.json b/package.json new file mode 100644 index 0000000..0ed5999 --- /dev/null +++ b/package.json @@ -0,0 +1,397 @@ +{ + "name": "coc-tsserver", + "version": "1.0.0", + "description": "tsserver extension for coc", + "main": "lib/index.js", + "publisher": "chemzqm", + "engines": { + "coc": "^0.0.15" + }, + "scripts": { + "clean": "rimraf lib", + "build": "tsc -p tsconfig.json", + "prepare": "yarn clean && yarn build" + }, + "contributes": { + "commands": [ + { + "title": "Reload current project", + "category": "TSServer", + "command": "tsserver.reloadProjects" + }, + { + "title": "Open log file of tsserver.", + "category": "TSServer", + "command": "tsserver.openTsServerLog" + }, + { + "title": "Open project config file.", + "category": "TSServer", + "command": "tsserver.goToProjectConfig" + }, + { + "title": "Restart tsserver.", + "category": "TSServer", + "command": "tsserver.restart" + }, + { + "title": "Format current buffer.", + "category": "TSServer", + "command": "tsserver.format" + }, + { + "title": "Orgnize imports of current buffer.", + "category": "TSServer", + "command": "tsserver.organizeImports" + }, + { + "title": "Run `tsc --watch` for current project in terminal buffer.", + "category": "TSServer", + "command": "tsserver.watchBuild" + } + ], + "configuration": { + "type": "object", + "title": "Tsserver", + "properties": { + "tsserver.enable": { + "type": "boolean", + "default": true, + "description": "Enable tsserver extension" + }, + "tsserver.locale": { + "type": "string", + "default": "", + "description": "Locale of tsserver" + }, + "tsserver.typingsCacheLocation": { + "type": "string", + "default": "", + "description": "Folder path for cache typings" + }, + "tsserver.formatOnSave": { + "type": "boolean", + "default": false, + "description": "Format document on buffer will save" + }, + "tsserver.orgnizeImportOnSave": { + "type": "boolean", + "default": false, + "description": "Orgnize import on buffer will save" + }, + "tsserver.formatOnType": { + "type": "boolean", + "default": true, + "description": "Run format on type special characters." + }, + "tsserver.enableJavascript": { + "type": "boolean", + "default": true, + "description": "Use tsserver for javascript files" + }, + "tsserver.tsdk": { + "type": "string", + "default": "", + "description": "Directory contains tsserver.js, works for workspace only" + }, + "tsserver.npm": { + "type": "string", + "default": "", + "description": "Executable path of npm for download typings" + }, + "tsserver.log": { + "type": "string", + "default": "off", + "enum": [ + "normal", + "terse", + "verbose", + "off" + ], + "description": "Log level of tsserver" + }, + "tsserver.trace.server": { + "type": "string", + "default": "off", + "enum": [ + "off", + "messages", + "verbose" + ], + "description": "Trace level of tsserver" + }, + "tserver.pluginNames": { + "type": "array", + "default": [], + "items": { + "type": "string" + }, + "description": "Module names of tsserver plugins" + }, + "tsserver.pluginRoot": { + "type": "string", + "default": "", + "description": "Folder contains tsserver plugins" + }, + "tsserver.debugPort": { + "type": "number", + "description": "Debug port number of tsserver" + }, + "tsserver.reportStyleChecksAsWarnings": { + "type": "boolean", + "default": true + }, + "tsserver.implicitProjectConfig.checkJs": { + "type": "boolean", + "default": false, + "description": "Enable checkJs for implicit project" + }, + "tsserver.implicitProjectConfig.experimentalDecorators": { + "type": "boolean", + "default": false, + "description": "Enable experimentalDecorators for implicit project" + }, + "tsserver.disableAutomaticTypeAcquisition": { + "type": "boolean", + "default": false, + "description": "Disable download of typings" + }, + "typescript.updateImportsOnFileMove.enable": { + "type": "boolean", + "default": true, + "description": "Enable update imports on file move." + }, + "typescript.implementationsCodeLens.enable": { + "type": "boolean", + "default": true, + "description": "Enable codeLens for implementations" + }, + "typescript.referencesCodeLens.enable": { + "type": "boolean", + "default": true, + "description": "Enable codeLens for references" + }, + "typescript.preferences.completion.useCodeSnippetsOnMethodSuggest": { + "type": "boolean", + "default": true, + "description": "Enable snippet for method suggestion" + }, + "typescript.preferences.completion.nameSuggestions": { + "type": "boolean", + "default": true, + "description": "Complete for warning type of tsserver" + }, + "typescript.preferences.completion.autoImportSuggestions": { + "type": "boolean", + "default": true, + "description": "Enable auto import suggestions for completion" + }, + "typescript.preferences.completion.commaAfterImport": { + "type": "boolean", + "default": true, + "description": "Add comma after import" + }, + "typescript.preferences.completion.moduleExports": { + "type": "boolean", + "default": true, + "description": "Include completion for module.exports" + }, + "typescript.preferences.importModuleSpecifier": { + "type": "string", + "default": "non-relative", + "enum": [ + "non-relative", + "relative" + ] + }, + "typescript.preferences.suggestionActions.enabled": { + "type": "boolean", + "default": true + }, + "typescript.preferences.quoteStyle": { + "type": "string", + "default": "single", + "enum": [ + "single", + "double" + ] + }, + "typescript.format.insertSpaceAfterCommaDelimiter": { + "type": "boolean", + "default": true + }, + "typescript.format.insertSpaceAfterConstructor": { + "type": "boolean", + "default": false + }, + "typescript.format.insertSpaceAfterSemicolonInForStatements": { + "type": "boolean", + "default": true + }, + "typescript.format.insertSpaceBeforeAndAfterBinaryOperators": { + "type": "boolean", + "default": true + }, + "typescript.format.insertSpaceAfterKeywordsInControlFlowStatements": { + "type": "boolean", + "default": true + }, + "typescript.format.insertSpaceAfterFunctionKeywordForAnonymousFunctions": { + "type": "boolean", + "default": true + }, + "typescript.format.insertSpaceBeforeFunctionParenthesis": { + "type": "boolean", + "default": false + }, + "typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis": { + "type": "boolean", + "default": false + }, + "typescript.format.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces": { + "type": "boolean", + "default": false + }, + "typescript.format.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces": { + "type": "boolean", + "default": false + }, + "typescript.format.insertSpaceAfterTypeAssertion": { + "type": "boolean", + "default": false + }, + "typescript.format.placeOpenBraceOnNewLineForFunctions": { + "type": "boolean", + "default": false + }, + "typescript.format.placeOpenBraceOnNewLineForControlBlocks": { + "type": "boolean", + "default": false + }, + "javascript.updateImportsOnFileMove.enable": { + "type": "boolean", + "default": true + }, + "javascript.implementationsCodeLens.enable": { + "type": "boolean", + "default": true + }, + "javascript.referencesCodeLens.enable": { + "type": "boolean", + "default": true + }, + "javascript.preferences.completion.useCodeSnippetsOnMethodSuggest": { + "type": "boolean", + "default": true + }, + "javascript.preferences.completion.nameSuggestions": { + "type": "boolean", + "default": true + }, + "javascript.preferences.completion.autoImportSuggestions": { + "type": "boolean", + "default": true + }, + "javascript.preferences.completion.commaAfterImport": { + "type": "boolean", + "default": true + }, + "javascript.preferences.completion.moduleExports": { + "type": "boolean", + "default": true, + "description": "Include completion for module.exports" + }, + "javascript.preferences.importModuleSpecifier": { + "type": "string", + "default": "non-relative", + "enum": [ + "non-relative", + "relative" + ] + }, + "javascript.preferences.suggestionActions.enabled": { + "type": "boolean", + "default": true + }, + "javascript.preferences.quoteStyle": { + "type": "string", + "default": "single", + "enum": [ + "single", + "double" + ] + }, + "javascript.format.insertSpaceAfterCommaDelimiter": { + "type": "boolean", + "default": true + }, + "javascript.format.insertSpaceAfterConstructor": { + "type": "boolean", + "default": false + }, + "javascript.format.insertSpaceAfterSemicolonInForStatements": { + "type": "boolean", + "default": true + }, + "javascript.format.insertSpaceBeforeAndAfterBinaryOperators": { + "type": "boolean", + "default": true + }, + "javascript.format.insertSpaceAfterKeywordsInControlFlowStatements": { + "type": "boolean", + "default": true + }, + "javascript.format.insertSpaceAfterFunctionKeywordForAnonymousFunctions": { + "type": "boolean", + "default": true + }, + "javascript.format.insertSpaceBeforeFunctionParenthesis": { + "type": "boolean", + "default": false + }, + "javascript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis": { + "type": "boolean", + "default": false + }, + "javascript.format.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces": { + "type": "boolean", + "default": false + }, + "javascript.format.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces": { + "type": "boolean", + "default": false + }, + "javascript.format.insertSpaceAfterTypeAssertion": { + "type": "boolean", + "default": false + }, + "javascript.format.placeOpenBraceOnNewLineForFunctions": { + "type": "boolean", + "default": false + }, + "javascript.format.placeOpenBraceOnNewLineForControlBlocks": { + "type": "boolean", + "default": false + } + } + } + }, + "author": "chemzqm@gmail.com", + "license": "MIT", + "devDependencies": { + "@chemzqm/tsconfig": "^0.0.3", + "@chemzqm/tslint-config": "^1.0.17", + "@types/node": "^10.9.4", + "coc.nvim": "^0.0.15", + "rimraf": "^2.6.2", + "tslint": "^5.11.0", + "typescript": "^3.0.3" + }, + "dependencies": { + "semver": "^5.5.1", + "tslib": "^1.9.3", + "vscode-languageserver-protocol": "^3.12.0", + "vscode-uri": "^1.0.6", + "which": "^1.3.1" + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..01be497 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,55 @@ +import { services, commands, languages, ExtensionContext, workspace, TextDocumentWillSaveEvent, ServiceStat } from 'coc.nvim' +import TsserverService from './server' +import { languageIds } from './server/utils/languageModeIds' +import { OpenTsServerLogCommand, ReloadProjectsCommand, TypeScriptGoToProjectConfigCommand } from './server/commands' +import { TextEdit } from 'vscode-languageserver-types' +import { Command } from './server/commands' + +export async function activate(context: ExtensionContext): Promise<void> { + let { subscriptions } = context + const config = workspace.getConfiguration().get('tsserver', {}) as any + if (!config.enable) return + const service = new TsserverService() + + subscriptions.push( + (services as any).regist(service) + ) + + function onWillSave(event: TextDocumentWillSaveEvent): void { + if (service.state != ServiceStat.Running) return + let config = service.config + let formatOnSave = config.get<boolean>('formatOnSave') + if (!formatOnSave) return + let { languageId } = event.document + if (languageIds.indexOf(languageId) == -1) return + let willSaveWaitUntil = async (): Promise<TextEdit[]> => { + let options = await workspace.getFormatOptions(event.document.uri) + let textEdits = await languages.provideDocumentFormattingEdits(event.document, options) + return textEdits + } + event.waitUntil(willSaveWaitUntil()) + } + + function registCommand(cmd: Command): void { + let { id, execute } = cmd + subscriptions.push(commands.registerCommand(id as string, execute, cmd)) + } + + registCommand(new ReloadProjectsCommand(service.clientHost)) + registCommand(new OpenTsServerLogCommand(service.clientHost)) + registCommand(new TypeScriptGoToProjectConfigCommand(service.clientHost)) + registCommand(commands.register({ + id: 'tsserver.restart', + execute: (): void => { + service.stop().then(() => { + setTimeout(() => { + service.restart() + }, 100) + }) + } + })) + + subscriptions.push( + workspace.onWillSaveUntil(onWillSave, null, 'tsserver') + ) +} diff --git a/src/server/LICENSE.txt b/src/server/LICENSE.txt new file mode 100644 index 0000000..9afc63d --- /dev/null +++ b/src/server/LICENSE.txt @@ -0,0 +1,23 @@ +MIT License + +Copyright (c) 2015 - present Microsoft Corporation + +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/server/commands.ts b/src/server/commands.ts new file mode 100644 index 0000000..2bd684d --- /dev/null +++ b/src/server/commands.ts @@ -0,0 +1,79 @@ +import { CancellationToken } from 'vscode-languageserver-protocol' +import URI from 'vscode-uri' +import { workspace } from 'coc.nvim' +import { ProjectInfoResponse } from './protocol' +import TypeScriptServiceClientHost from './typescriptServiceClientHost' + +export interface Command { + readonly id: string | string[] + execute(...args: any[]): void | Promise<any> +} + +export class ReloadProjectsCommand implements Command { + public readonly id = 'tsserver.reloadProjects' + + public constructor( + private readonly client: TypeScriptServiceClientHost + ) { } + + public execute(): void { + this.client.reloadProjects() + workspace.showMessage('projects reloaded') + } +} + +export class OpenTsServerLogCommand implements Command { + public readonly id = 'tsserver.openTsServerLog' + + public constructor( + private readonly client: TypeScriptServiceClientHost + ) { } + + public execute(): void { + this.client.serviceClient.openTsServerLogFile() // tslint:disable-line + } +} + +export class TypeScriptGoToProjectConfigCommand implements Command { + public readonly id = 'tsserver.goToProjectConfig' + + public constructor( + private readonly client: TypeScriptServiceClientHost + ) { } + + public async execute(): Promise<void> { + let doc = await workspace.document + await goToProjectConfig(this.client, doc.uri) + } +} + +async function goToProjectConfig(clientHost: TypeScriptServiceClientHost, uri: string): Promise<void> { + if (!clientHost.handles(uri)) { + workspace.showMessage('Could not determine TypeScript or JavaScript project. Unsupported file type', 'warning') + return + } + const client = clientHost.serviceClient + const file = client.toPath(uri) + let res: ProjectInfoResponse | undefined + try { + res = await client.execute('projectInfo', { file, needFileNameList: false }, CancellationToken.None) + } catch { + // noop + } + if (!res || !res.body) { + workspace.showMessage('Could not determine TypeScript or JavaScript project.', 'warning') + return + } + + const { configFileName } = res.body + if (configFileName && !isImplicitProjectConfigFile(configFileName)) { + await workspace.openResource(URI.file(configFileName).toString()) + return + } + + workspace.showMessage('Config file not found', 'warning') +} + +function isImplicitProjectConfigFile(configFileName: string): boolean { + return configFileName.indexOf('/dev/null/') === 0 +} diff --git a/src/server/features/baseCodeLensProvider.ts b/src/server/features/baseCodeLensProvider.ts new file mode 100644 index 0000000..e906159 --- /dev/null +++ b/src/server/features/baseCodeLensProvider.ts @@ -0,0 +1,152 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { CancellationToken, CodeLens, Emitter, Event, Range, TextDocument } from 'vscode-languageserver-protocol' +import { CodeLensProvider } from 'coc.nvim/lib/provider' +import * as Proto from '../protocol' +import { ITypeScriptServiceClient } from '../typescriptService' +import { escapeRegExp } from '../utils/regexp' +import * as typeConverters from '../utils/typeConverters' + +export class CachedNavTreeResponse { + private response?: Promise<Proto.NavTreeResponse> + private version = -1 + private document = '' + + public execute(document: TextDocument, f: () => Promise<Proto.NavTreeResponse>): Promise<Proto.NavTreeResponse> { + if (this.matches(document)) { + return this.response + } + + return this.update(document, f()) + } + + private matches(document: TextDocument): boolean { + return ( + this.version === document.version && + this.document === document.uri.toString() + ) + } + + private update( + document: TextDocument, + response: Promise<Proto.NavTreeResponse> + ): Promise<Proto.NavTreeResponse> { + this.response = response + this.version = document.version + this.document = document.uri.toString() + return response + } +} + +export abstract class TypeScriptBaseCodeLensProvider implements CodeLensProvider { + private onDidChangeCodeLensesEmitter = new Emitter<void>() + + public constructor( + protected client: ITypeScriptServiceClient, + private cachedResponse: CachedNavTreeResponse + ) { } + + public get onDidChangeCodeLenses(): Event<void> { + return this.onDidChangeCodeLensesEmitter.event + } + + public async provideCodeLenses( + document: TextDocument, + token: CancellationToken + ): Promise<CodeLens[]> { + const filepath = this.client.toPath(document.uri) + if (!filepath) { + return [] + } + + try { + const response = await this.cachedResponse.execute(document, () => + this.client.execute('navtree', { file: filepath }, token) + ) + if (!response) { + return [] + } + + const tree = response.body + const referenceableSpans: Range[] = [] + if (tree && tree.childItems) { + tree.childItems.forEach(item => + this.walkNavTree(document, item, null, referenceableSpans) + ) + } + return referenceableSpans.map( + range => { + return { + range, + data: { uri: document.uri } + } + } + ) + } catch { + return [] + } + } + + protected abstract extractSymbol( + document: TextDocument, + item: Proto.NavigationTree, + parent: Proto.NavigationTree | null + ): Range | null + + private walkNavTree( + document: TextDocument, + item: Proto.NavigationTree, + parent: Proto.NavigationTree | null, + results: Range[] + ): void { + if (!item) { + return + } + + const range = this.extractSymbol(document, item, parent) + if (range) { + results.push(range) + } + if (item.childItems) { + item.childItems.forEach(child => + this.walkNavTree(document, child, item, results) + ) + } + } + protected getSymbolRange( + document: TextDocument, + item: Proto.NavigationTree + ): Range | null { + if (!item) { + return null + } + + // TS 3.0+ provides a span for just the symbol + if ((item as any).nameSpan) { + return typeConverters.Range.fromTextSpan((item as any).nameSpan) + } + + // In older versions, we have to calculate this manually. See #23924 + const span = item.spans && item.spans[0] + if (!span) { + return null + } + + const range = typeConverters.Range.fromTextSpan(span) + const text = document.getText(range) + + const identifierMatch = new RegExp( + `^(.*?(\\b|\\W))${escapeRegExp(item.text || '')}(\\b|\\W)`, + 'gm' + ) + const match = identifierMatch.exec(text) + const prefixLength = match ? match.index + match[1].length : 0 + const startOffset = document.offsetAt(range.start) + prefixLength + return { + start: document.positionAt(startOffset), + end: document.positionAt(startOffset + item.text.length) + } + } +} diff --git a/src/server/features/bufferSyncSupport.ts b/src/server/features/bufferSyncSupport.ts new file mode 100644 index 0000000..7fcaf73 --- /dev/null +++ b/src/server/features/bufferSyncSupport.ts @@ -0,0 +1,191 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { DidChangeTextDocumentParams, Disposable, TextDocument } from 'vscode-languageserver-protocol' +import { disposeAll, workspace } from 'coc.nvim' +import Proto from '../protocol' +import { ITypeScriptServiceClient } from '../typescriptService' +import API from '../utils/api' +import { Delayer } from '../utils/async' +import * as languageModeIds from '../utils/languageModeIds' + +function mode2ScriptKind( + mode: string +): 'TS' | 'TSX' | 'JS' | 'JSX' | undefined { + switch (mode) { + case languageModeIds.typescript: + return 'TS' + case languageModeIds.typescripttsx: + return 'TSX' + case languageModeIds.typescriptjsx: + return 'TSX' + case languageModeIds.typescriptreact: + return 'TSX' + case languageModeIds.javascript: + return 'JS' + case languageModeIds.javascriptreact: + return 'JSX' + } + return undefined +} + +export default class BufferSyncSupport { + private readonly client: ITypeScriptServiceClient + + private _validate: boolean + private readonly modeIds: Set<string> + private readonly uris: Set<string> = new Set() + private readonly disposables: Disposable[] = [] + + private readonly pendingDiagnostics = new Map<string, number>() + private readonly diagnosticDelayer: Delayer<any> + + constructor( + client: ITypeScriptServiceClient, + modeIds: string[], + validate: boolean + ) { + this.client = client + this.modeIds = new Set<string>(modeIds) + this._validate = validate || false + this.diagnosticDelayer = new Delayer<any>(300) + } + + public listen(): void { + workspace.onDidOpenTextDocument( + this.onDidOpenTextDocument, + this, + this.disposables + ) + workspace.onDidCloseTextDocument( + this.onDidCloseTextDocument, + this, + this.disposables + ) + workspace.onDidChangeTextDocument( + this.onDidChangeTextDocument, + this, + this.disposables + ) + workspace.textDocuments.forEach(this.onDidOpenTextDocument, this) + } + + public reInitialize(): void { + workspace.textDocuments.forEach(this.onDidOpenTextDocument, this) + } + + public set validate(value: boolean) { + this._validate = value + } + + public dispose(): void { + this.pendingDiagnostics.clear() + disposeAll(this.disposables) + } + + private onDidOpenTextDocument(document: TextDocument): void { + if (!this.modeIds.has(document.languageId)) return + let { uri } = document + let filepath = this.client.toPath(uri) + this.uris.add(uri) + const args: Proto.OpenRequestArgs = { + file: filepath, + fileContent: document.getText() + } + + if (this.client.apiVersion.gte(API.v203)) { + const scriptKind = mode2ScriptKind(document.languageId) + if (scriptKind) { + args.scriptKindName = scriptKind + } + } + this.client.execute('open', args, false) // tslint:disable-line + this.requestDiagnostic(uri) + } + + private onDidCloseTextDocument(document: TextDocument): void { + let { uri } = document + if (!this.uris.has(uri)) return + let filepath = this.client.toPath(uri) + const args: Proto.FileRequestArgs = { + file: filepath + } + this.client.execute('close', args, false) // tslint:disable-line + } + + private onDidChangeTextDocument(e: DidChangeTextDocumentParams): void { + let { textDocument, contentChanges } = e + let { uri } = textDocument + if (!this.uris.has(uri)) return + let filepath = this.client.toPath(uri) + for (const { range, text } of contentChanges) { + const args: Proto.ChangeRequestArgs = { + file: filepath, + line: range ? range.start.line + 1 : 1, + offset: range ? range.start.character + 1 : 1, + endLine: range ? range.end.line + 1 : 2 ** 24, + endOffset: range ? range.end.character + 1 : 1, + insertString: text + } + this.client.execute('change', args, false) // tslint:disable-line + } + this.requestDiagnostic(uri) + } + + public requestAllDiagnostics(): void { + if (!this._validate) { + return + } + for (const uri of this.uris) { + this.pendingDiagnostics.set(uri, Date.now()) + } + this.diagnosticDelayer.trigger(() => { // tslint:disable-line + this.sendPendingDiagnostics() + }, 200) + } + + public requestDiagnostic(uri: string): void { + if (!this._validate) { + return + } + let document = workspace.getDocument(uri) + if (!document) return + this.pendingDiagnostics.set(uri, Date.now()) + let delay = 300 + const lineCount = document.lineCount + delay = Math.min(Math.max(Math.ceil(lineCount / 20), 300), 800) + this.diagnosticDelayer.trigger(() => { + this.sendPendingDiagnostics() + }, delay) // tslint:disable-line + } + + public hasPendingDiagnostics(uri: string): boolean { + return this.pendingDiagnostics.has(uri) + } + + private sendPendingDiagnostics(): void { + if (!this._validate) { + return + } + const files = Array.from(this.pendingDiagnostics.entries()) + .sort((a, b) => a[1] - b[1]) + .map(entry => this.client.toPath(entry[0])) + + // Add all open TS buffers to the geterr request. They might be visible + for (const uri of this.uris) { + if (!this.pendingDiagnostics.get(uri)) { + let file = this.client.toPath(uri) + files.push(file) + } + } + if (files.length) { + const args: Proto.GeterrRequestArgs = { + delay: 0, + files + } + this.client.execute('geterr', args, false) // tslint:disable-line + } + this.pendingDiagnostics.clear() + } +} diff --git a/src/server/features/completionItemProvider.ts b/src/server/features/completionItemProvider.ts new file mode 100644 index 0000000..5acd8b3 --- /dev/null +++ b/src/server/features/completionItemProvider.ts @@ -0,0 +1,375 @@ +/*--------------------------------------------------------------------------------------------- + * 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, TextDocument, TextEdit } from 'vscode-languageserver-protocol' +import { commands, workspace } from 'coc.nvim' +import { CompletionItemProvider } from 'coc.nvim/lib/provider' +import Proto from '../protocol' +import * as PConst from '../protocol.const' +import { ITypeScriptServiceClient } from '../typescriptService' +import API from '../utils/api' +import { applyCodeAction } from '../utils/codeAction' +import { convertCompletionEntry, resolveItem } from '../utils/completionItem' +import * as Previewer from '../utils/previewer' +import * as typeConverters from '../utils/typeConverters' +import TypingsStatus from '../utils/typingsStatus' +import FileConfigurationManager, { CompletionOptions } from './fileConfigurationManager' + +// command center +export interface CommandItem { + readonly id: string | string[] + execute(...args: any[]): void | Promise<any> +} + +class ApplyCompletionCodeActionCommand implements CommandItem { + public static readonly ID = '_typescript.applyCompletionCodeAction' + public readonly id = ApplyCompletionCodeActionCommand.ID + public constructor( + private readonly client: ITypeScriptServiceClient + ) { + } + + // apply code action on complete + public async execute(codeActions: Proto.CodeAction[]): Promise<void> { + if (codeActions.length === 0) { + return + } + if (codeActions.length === 1) { + await applyCodeAction(this.client, codeActions[0]) + return + } + const idx = await workspace.showQuickpick(codeActions.map(o => o.description), 'Select code action to apply') + if (idx < 0) return + const action = codeActions[idx] + await applyCodeAction(this.client, action) + return + } +} + +export default class TypeScriptCompletionItemProvider implements CompletionItemProvider { + + public static readonly triggerCharacters = ['.', '@', '<'] + private completeOption: CompletionOptions + + constructor( + private readonly client: ITypeScriptServiceClient, + private readonly typingsStatus: TypingsStatus, + private readonly fileConfigurationManager: FileConfigurationManager, + languageId: string + ) { + + this.setCompleteOption(languageId) + commands.register(new ApplyCompletionCodeActionCommand(this.client)) + } + + private setCompleteOption(languageId: string): void { + this.completeOption = this.fileConfigurationManager.getCompleteOptions(languageId) + } + + /** + * Get completionItems + * + * @public + * @param {TextDocument} document + * @param {Position} position + * @param {CancellationToken} token + * @param {string} triggerCharacter + * @returns {Promise<CompletionItem[]>} + */ + public async provideCompletionItems( + document: TextDocument, + position: Position, + token: CancellationToken, + context: CompletionContext, + ): Promise<CompletionItem[]> { + if (this.typingsStatus.isAcquiringTypings) { + workspace.showMessage('Acquiring typings...', 'warning') + return [] + } + let { uri } = document + const file = this.client.toPath(document.uri) + if (!file) return [] + let preText = document.getText({ + start: { line: position.line, character: 0 }, + end: position + }) + let { triggerCharacter } = context + + if (!this.shouldTrigger(triggerCharacter, preText)) { + return [] + } + + const { completeOption } = this + + const args: Proto.CompletionsRequestArgs = { + ...typeConverters.Position.toFileLocationRequestArgs(file, position), + includeExternalModuleExports: completeOption.autoImportSuggestions, + includeInsertTextCompletions: true, + triggerCharacter: triggerCharacter && triggerCharacter === '.' ? triggerCharacter : undefined + } + + let msg: Proto.CompletionEntry[] | undefined + try { + const response = await this.client.execute('completions', args, token) + msg = response.body + if (!msg) { + return [] + } + } catch { + return [] + } + + const completionItems: CompletionItem[] = [] + for (const element of msg) { + let { kind } = element + if (kind === PConst.Kind.warning + || kind === PConst.Kind.script) { + if (!completeOption.nameSuggestions || triggerCharacter == '.') { + continue + } + } + if (!completeOption.autoImportSuggestions && element.hasAction) { + continue + } + const item = convertCompletionEntry( + element, + uri, + position, + completeOption.useCodeSnippetsOnMethodSuggest, + ) + completionItems.push(item) + } + + return completionItems + } + + /** + * Resolve complete item, could have documentation added + * + * @public + * @param {CompletionItem} item + * @param {CancellationToken} token + * @returns {Promise<CompletionItem>} + */ + public async resolveCompletionItem( + item: CompletionItem, + token: CancellationToken + ): Promise<CompletionItem> { + if (item == null) return undefined + + let { uri, position, source } = item.data + const filepath = this.client.toPath(uri) + if (!filepath) return undefined + let document = workspace.getDocument(uri) + if (!document) return undefined + resolveItem(item, document) + const args: Proto.CompletionDetailsRequestArgs = { + ...typeConverters.Position.toFileLocationRequestArgs( + filepath, + position + ), + entryNames: [ + source + ? { name: item.label, source } + : item.label + ] + } + + let response: Proto.CompletionDetailsResponse + try { + response = await this.client.execute( + 'completionEntryDetails', + args, + token + ) + } catch { + return item + } + + const details = response.body + if (!details || !details.length || !details[0]) { + return item + } + const detail = details[0] + item.detail = detail.displayParts.length + ? Previewer.plain(detail.displayParts) + : undefined + + item.documentation = this.getDocumentation(detail) + const { command, additionalTextEdits } = this.getCodeActions(detail, filepath) + if (command) item.command = command + item.additionalTextEdits = additionalTextEdits + if (detail && item.insertTextFormat == InsertTextFormat.Snippet) { + this.createSnippetOfFunctionCall(item, detail) + } + + return item + } + + private getCodeActions( + detail: Proto.CompletionEntryDetails, + filepath: string + ): { command?: Command; additionalTextEdits?: TextEdit[] } { + if (!detail.codeActions || !detail.codeActions.length) { + return {} + } + let { commaAfterImport } = this.completeOption + // Try to extract out the additionalTextEdits for the current file. + // Also check if we still have to apply other workspace edits + const additionalTextEdits: TextEdit[] = [] + let hasReaminingCommandsOrEdits = false + for (const tsAction of detail.codeActions) { + if (tsAction.commands) { + hasReaminingCommandsOrEdits = true + } + // Convert all edits in the current file using `additionalTextEdits` + if (tsAction.changes) { + for (const change of tsAction.changes) { + if (change.fileName === filepath) { + additionalTextEdits.push( + ...change.textChanges.map(typeConverters.TextEdit.fromCodeEdit) + ) + } else { + hasReaminingCommandsOrEdits = true + } + } + } + } + + let command = null + + if (hasReaminingCommandsOrEdits) { + // Create command that applies all edits not in the current file. + command = { + title: '', + command: ApplyCompletionCodeActionCommand.ID, + arguments: [ + detail.codeActions.map((x): Proto.CodeAction => ({ + commands: x.commands, + description: x.description, + changes: x.changes.filter(x => x.fileName !== filepath) + })) + ] + } + } + if (additionalTextEdits.length && !commaAfterImport) { + // remove comma + additionalTextEdits.forEach(o => { + o.newText = o.newText.replace(/;/, '') + }) + } + return { + command, + additionalTextEdits: additionalTextEdits.length + ? additionalTextEdits + : undefined + } + } + + private shouldTrigger( + triggerCharacter: string, + pre: string, + ): boolean { + if (triggerCharacter === '.') { + if (pre.match(/[\s\.'"]\.$/)) { + return false + } + } else if (triggerCharacter === '@') { + // make sure we are in something that looks like the start of a jsdoc comment + if (!pre.match(/^\s*\*[ ]?@/) && !pre.match(/\/\*\*+[ ]?@/)) { + return false + } + } else if (triggerCharacter === '<') { + return this.client.apiVersion.gte(API.v290) + } + + return true + } + + // complete item documentation + private getDocumentation(detail: Proto.CompletionEntryDetails): MarkupContent | undefined { + let documentation = '' + if (detail.source) { + const importPath = `'${Previewer.plain(detail.source)}'` + const autoImportLabel = `Auto import from ${importPath}` + documentation += `${autoImportLabel}\n` + } + let parts = [ + Previewer.plain(detail.documentation), + Previewer.tagsMarkdownPreview(detail.tags) + ] + parts = parts.filter(s => s && s.trim() != '') + documentation += parts.join('\n\n') + if (documentation.length) { + return { + kind: MarkupKind.Markdown, + value: documentation + } + } + return undefined + } + + private createSnippetOfFunctionCall( + item: CompletionItem, + detail: Proto.CompletionEntryDetails + ): void { + let hasOptionalParameters = false + let hasAddedParameters = false + + let snippet = '' + const methodName = detail.displayParts.find( + part => part.kind === 'methodName' + ) + let { textEdit, data } = item + let { position, uri } = data + + if (textEdit) { + snippet += item.insertText || textEdit.newText // tslint:disable-line + } else { + let document = workspace.getDocument(uri) + if (!document) return + let range = document.getWordRangeAtPosition(position) + textEdit = { range, newText: '' } + snippet += item.insertText || (methodName && methodName.text) || item.label // tslint:disable-line + } + snippet += '(' + let holderIndex = 1 + let parenCount = 0 + let i = 0 + for (; i < detail.displayParts.length; ++i) { + const part = detail.displayParts[i] + // Only take top level paren names + if (part.kind === 'parameterName' && parenCount === 1) { + const next = detail.displayParts[i + 1] + // Skip optional parameters + const nameIsFollowedByOptionalIndicator = next && next.text === '?' + if (!nameIsFollowedByOptionalIndicator) { + if (hasAddedParameters) snippet += ', ' + hasAddedParameters = true + snippet += '${' + holderIndex + ':' + part.text + '}' + holderIndex = holderIndex + 1 + } + hasOptionalParameters = + hasOptionalParameters || nameIsFollowedByOptionalIndicator + } else if (part.kind === 'punctuation') { + if (part.text === '(') { + ++parenCount + } else if (part.text === ')') { + --parenCount + } else if (part.text === '...' && parenCount === 1) { + // Found rest parmeter. Do not fill in any further arguments + hasOptionalParameters = true + break + } + } + } + if (hasOptionalParameters) { + snippet += '${' + holderIndex + '}' + } + snippet += ')' + snippet += '$0' + textEdit.newText = snippet + item.textEdit = textEdit + } +} diff --git a/src/server/features/definitionProvider.ts b/src/server/features/definitionProvider.ts new file mode 100644 index 0000000..63a5e18 --- /dev/null +++ b/src/server/features/definitionProvider.ts @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { CancellationToken, Definition, Location, Position, TextDocument } from 'vscode-languageserver-protocol' +import { DefinitionProvider, ImplementationProvider, TypeDefinitionProvider } from 'coc.nvim/lib/provider' +import * as Proto from '../protocol' +import { ITypeScriptServiceClient } from '../typescriptService' +import * as typeConverters from '../utils/typeConverters' + +export default class TypeScriptDefinitionProvider implements DefinitionProvider, TypeDefinitionProvider, ImplementationProvider { + constructor(private client: ITypeScriptServiceClient) { } + + protected async getSymbolLocations( + definitionType: 'definition' | 'implementation' | 'typeDefinition', + document: TextDocument, + position: Position, + token: CancellationToken | boolean + ): Promise<Location[] | undefined> { + const filepath = this.client.toPath(document.uri) + if (!filepath) { + return undefined + } + + const args = typeConverters.Position.toFileLocationRequestArgs( + filepath, + position + ) + try { + const response = await this.client.execute(definitionType, args, token) + const locations: Proto.FileSpan[] = (response && response.body) || [] + return locations.map(location => + typeConverters.Location.fromTextSpan( + this.client.toResource(location.file), + location + ) + ) + } catch { + return [] + } + } + + public provideDefinition( + document: TextDocument, + position: Position, + token: CancellationToken | boolean + ): Promise<Definition | undefined> { + return this.getSymbolLocations('definition', document, position, token) + } + + public provideTypeDefinition( + document: TextDocument, + position: Position, + token: CancellationToken): Promise<Definition> { + return this.getSymbolLocations('typeDefinition', document, position, token) + } + + public provideImplementation( + document: TextDocument, + position: Position, + token: CancellationToken): Promise<Definition> { + return this.getSymbolLocations('implementation', document, position, token) + } +} diff --git a/src/server/features/diagnostics.ts b/src/server/features/diagnostics.ts new file mode 100644 index 0000000..a956177 --- /dev/null +++ b/src/server/features/diagnostics.ts @@ -0,0 +1,161 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { Diagnostic } from 'vscode-languageserver-protocol' +import { languages, DiagnosticCollection } from 'coc.nvim' +import { ResourceMap } from './resourceMap' + +export class DiagnosticSet { + private _map = new ResourceMap<Diagnostic[]>() + + public set(uri: string, diagnostics: Diagnostic[]): void { + this._map.set(uri, diagnostics) + } + + public get(uri: string): Diagnostic[] { + return this._map.get(uri) || [] + } + + public clear(): void { + this._map = new ResourceMap<Diagnostic[]>() + } +} + +export enum DiagnosticKind { + Syntax, + Semantic, + Suggestion +} + +const allDiagnosticKinds = [ + DiagnosticKind.Syntax, + DiagnosticKind.Semantic, + DiagnosticKind.Suggestion +] + +export class DiagnosticsManager { + private readonly _diagnostics = new Map<DiagnosticKind, DiagnosticSet>() + private readonly _currentDiagnostics: DiagnosticCollection + private _pendingUpdates = new ResourceMap<any>() + private _validate = true + private _enableSuggestions = true + + private readonly updateDelay = 200 + + constructor() { + for (const kind of allDiagnosticKinds) { + this._diagnostics.set(kind, new DiagnosticSet()) + } + this._currentDiagnostics = languages.createDiagnosticCollection('tsserver') + } + + public dispose(): void { + this._currentDiagnostics.dispose() + for (const value of this._pendingUpdates.values) { + clearTimeout(value) + } + this._pendingUpdates = new ResourceMap<any>() + } + + public reInitialize(): void { + this._currentDiagnostics.clear() + for (const diagnosticSet of this._diagnostics.values()) { + diagnosticSet.clear() + } + } + + public set validate(value: boolean) { + if (this._validate === value) { + return + } + + this._validate = value + if (!value) { + this._currentDiagnostics.clear() + } + } + + public set enableSuggestions(value: boolean) { + if (this._enableSuggestions === value) { + return + } + + this._enableSuggestions = value + if (!value) { + this._currentDiagnostics.clear() + } + } + + public diagnosticsReceived( + kind: DiagnosticKind, + uri: string, + diagnostics: Diagnostic[] + ): void { + const collection = this._diagnostics.get(kind) + if (!collection) { + return + } + + if (diagnostics.length === 0) { + const existing = collection.get(uri) + if (existing.length === 0) { + // No need to update + return + } + } + + collection.set(uri, diagnostics) + + this.scheduleDiagnosticsUpdate(uri) + } + + public delete(uri: string): void { + this._currentDiagnostics.delete(uri) + } + + public getDiagnostics(uri: string): Diagnostic[] { + return this._currentDiagnostics.get(uri) || [] + return [] + } + + private scheduleDiagnosticsUpdate(uri: string): void { + if (!this._pendingUpdates.has(uri)) { + this._pendingUpdates.set( + uri, + setTimeout(() => this.updateCurrentDiagnostics(uri), this.updateDelay) + ) + } + } + + private updateCurrentDiagnostics(uri: string): void { + if (this._pendingUpdates.has(uri)) { + clearTimeout(this._pendingUpdates.get(uri)) + this._pendingUpdates.delete(uri) + } + + if (!this._validate) { + return + } + + const allDiagnostics = [ + ...this._diagnostics.get(DiagnosticKind.Syntax)!.get(uri), + ...this._diagnostics.get(DiagnosticKind.Semantic)!.get(uri), + ...this.getSuggestionDiagnostics(uri) + ] + this._currentDiagnostics.set(uri, allDiagnostics) + } + + private getSuggestionDiagnostics(uri: string): Diagnostic[] { + return this._diagnostics + .get(DiagnosticKind.Suggestion)! + .get(uri) + .filter(x => { + if (!this._enableSuggestions) { + // Still show unused + return x.code && x.code == 6133 + } + return true + }) + } +} diff --git a/src/server/features/directiveCommentCompletions.ts b/src/server/features/directiveCommentCompletions.ts new file mode 100644 index 0000000..b04ea76 --- /dev/null +++ b/src/server/features/directiveCommentCompletions.ts @@ -0,0 +1,76 @@ +/*--------------------------------------------------------------------------------------------- + * 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, TextDocument } from 'vscode-languageserver-protocol' +import { workspace } from 'coc.nvim' +import { ITypeScriptServiceClient } from '../typescriptService' + +interface Directive { + readonly value: string + readonly description: string +} + +const directives: Directive[] = [ + { + value: '@ts-check', + description: 'Enables semantic checking in a JavaScript file. Must be at the top of a file.' + }, + { + value: '@ts-nocheck', + description: 'Disables semantic checking in a JavaScript file. Must be at the top of a file.' + }, + { + value: '@ts-ignore', + description: 'Suppresses @ts-check errors on the next line of a file.' + } +] + +export default class DirectiveCommentCompletionProvider { + constructor(private readonly client: ITypeScriptServiceClient) { } + + public provideCompletionItems( + document: TextDocument, + position: Position, + _token: CancellationToken, + context: CompletionContext + ): CompletionItem[] | CompletionList { + if (context.triggerCharacter != '@') { + return [] + } + const file = this.client.toPath(document.uri) + if (!file) { + return [] + } + const doc = workspace.getDocument(document.uri) + + const line = doc.getline(position.line) + const prefix = line.slice(0, position.character) + const match = prefix.match(/^\s*\/\/+\s?(@[a-zA-Z\-]*)?$/) + if (match) { + let items = directives.map(directive => { + const item = CompletionItem.create(directive.value) + item.kind = CompletionItemKind.Snippet + item.detail = directive.description + item.textEdit = { + range: Range.create( + position.line, + Math.max(0, position.character - (match[1] ? match[1].length : 0)), + position.line, + position.character + ), + newText: directive.value + } + return item + }) + let res: any = { + isIncomplete: false, + items + } + res.startcol = doc.fixStartcol(position, ['@']) + return res as any + } + return [] + } +} diff --git a/src/server/features/documentHighlight.ts b/src/server/features/documentHighlight.ts new file mode 100644 index 0000000..5a9d04e --- /dev/null +++ b/src/server/features/documentHighlight.ts @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { CancellationToken, DocumentHighlight, DocumentHighlightKind, Position, TextDocument } from 'vscode-languageserver-protocol' +import { DocumentHighlightProvider } from 'coc.nvim/lib/provider' +import Proto from '../protocol' +import { ITypeScriptServiceClient } from '../typescriptService' +import * as typeConverters from '../utils/typeConverters' + +export default class TypeScriptDocumentHighlightProvider implements DocumentHighlightProvider { + public constructor(private readonly client: ITypeScriptServiceClient) { } + + public async provideDocumentHighlights( + resource: TextDocument, + position: Position, + token: CancellationToken + ): Promise<DocumentHighlight[]> { + const file = this.client.toPath(resource.uri) + if (!file) return [] + + const args = typeConverters.Position.toFileLocationRequestArgs( + file, + position + ) + try { + const response = await this.client.execute('occurrences', args, token) + if (response && response.body) { + return response.body + .filter(x => !x.isInString) + .map(documentHighlightFromOccurance) + } + } catch { + // noop + } + + return [] + } +} + +function documentHighlightFromOccurance( + occurrence: Proto.OccurrencesResponseItem // tslint:disable-line +): DocumentHighlight { + return { + range: typeConverters.Range.fromTextSpan(occurrence), + kind: occurrence.isWriteAccess ? DocumentHighlightKind.Write : DocumentHighlightKind.Read + } +} diff --git a/src/server/features/documentSymbol.ts b/src/server/features/documentSymbol.ts new file mode 100644 index 0000000..0d2078f --- /dev/null +++ b/src/server/features/documentSymbol.ts @@ -0,0 +1,143 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { CancellationToken, DocumentSymbol, Range, SymbolKind, TextDocument } from 'vscode-languageserver-protocol' +import { DocumentSymbolProvider } from 'coc.nvim/lib/provider' +import * as Proto from '../protocol' +import * as PConst from '../protocol.const' +import { ITypeScriptServiceClient } from '../typescriptService' +import * as typeConverters from '../utils/typeConverters' + +const getSymbolKind = (kind: string): SymbolKind => { + switch (kind) { + case PConst.Kind.module: + return SymbolKind.Module + case PConst.Kind.class: + return SymbolKind.Class + case PConst.Kind.enum: + return SymbolKind.Enum + case PConst.Kind.interface: + return SymbolKind.Interface + case PConst.Kind.memberFunction: + return SymbolKind.Method + case PConst.Kind.memberVariable: + return SymbolKind.Property + case PConst.Kind.memberGetAccessor: + return SymbolKind.Property + case PConst.Kind.memberSetAccessor: + return SymbolKind.Property + case PConst.Kind.variable: + return SymbolKind.Variable + case PConst.Kind.const: + return SymbolKind.Variable + case PConst.Kind.localVariable: + return SymbolKind.Variable + case PConst.Kind.variable: + return SymbolKind.Variable + case PConst.Kind.constructSignature: + case PConst.Kind.constructorImplementation: + case PConst.Kind.function: + case PConst.Kind.localFunction: + return SymbolKind.Function + } + return SymbolKind.Variable +} + +export default class TypeScriptDocumentSymbolProvider implements DocumentSymbolProvider { + public constructor(private readonly client: ITypeScriptServiceClient) { } + + public async provideDocumentSymbols( + resource: TextDocument, + token: CancellationToken + ): Promise<DocumentSymbol[]> { + const filepath = this.client.toPath(resource.uri) + if (!filepath) return [] + + const args: Proto.FileRequestArgs = { + file: filepath + } + + try { + const response = await this.client.execute('navtree', args, token) + if (response.body) { + // The root represents the file. Ignore this when showing in the UI + const tree = response.body + if (tree.childItems) { + const result = new Array<DocumentSymbol>() + tree.childItems.forEach(item => + TypeScriptDocumentSymbolProvider.convertNavTree( + result, + item + ) + ) + return result + } + } + return [] + } catch (e) { + return [] + } + } + + private static convertNavTree( + bucket: 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 + + for (const child of children) { + if (child.spans.some(span => !!containsRange(range, typeConverters.Range.fromTextSpan(span)))) { + const includedChild = TypeScriptDocumentSymbolProvider.convertNavTree(symbolInfo.children, child) + shouldInclude = shouldInclude || includedChild + children.delete(child) + } + } + + if (shouldInclude) { + bucket.push(symbolInfo) + } + } + + return shouldInclude + } + + private static shouldInclueEntry( + item: Proto.NavigationTree | Proto.NavigationBarItem + ): boolean { + if (item.kind === PConst.Kind.alias) { + return false + } + return !!( + item.text && + item.text !== '<function>' && + item.text !== '<class>' + ) + } +} + +function containsRange(range: Range, otherRange: Range): boolean { + if (otherRange.start.line < range.start.line || otherRange.end.line < range.start.line) { + return false + } + if (otherRange.start.line > range.end.line || otherRange.end.line > range.end.line) { + return false + } + if (otherRange.start.line === range.start.line && otherRange.start.character < range.start.character) { + return false + } + if (otherRange.end.line === range.end.line && otherRange.end.character > range.end.character) { + return false + } + return true +} diff --git a/src/server/features/fileConfigurationManager.ts b/src/server/features/fileConfigurationManager.ts new file mode 100644 index 0000000..dce4a4d --- /dev/null +++ b/src/server/features/fileConfigurationManager.ts @@ -0,0 +1,175 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { TextDocument } from 'vscode-languageserver-protocol' +import { WorkspaceConfiguration, workspace } from 'coc.nvim' +import Proto from '../protocol' +import { ITypeScriptServiceClient } from '../typescriptService' +import API from '../utils/api' +import * as languageIds from '../utils/languageModeIds' + +function objAreEqual<T>(a: T, b: T): boolean { + let keys = Object.keys(a) + for (let i = 0; i < keys.length; i++) { // tslint:disable-line + let key = keys[i] + if ((a as any)[key] !== (b as any)[key]) { + return false + } + } + return true +} + +interface FormatOptions { + tabSize: number + insertSpaces: boolean +} + +interface FileConfiguration { + formatOptions: Proto.FormatCodeSettings + preferences: Proto.UserPreferences +} + +export interface CompletionOptions { + readonly commaAfterImport: boolean + readonly useCodeSnippetsOnMethodSuggest: boolean + readonly nameSuggestions: boolean + readonly autoImportSuggestions: boolean +} + +export default class FileConfigurationManager { + private cachedOption = null + private requesting = false + + public constructor(private readonly client: ITypeScriptServiceClient) { + } + + public async ensureConfigurationOptions(languageId: string, insertSpaces: boolean, tabSize: number): Promise<void> { + let { requesting } = this + let options: FormatOptions = { + tabSize, + insertSpaces + } + if (requesting || (this.cachedOption && objAreEqual(this.cachedOption, options))) return + const currentOptions = this.getFileOptions(options, languageId) + this.requesting = true + const args = { + hostInfo: 'nvim-coc', + ...currentOptions + } as Proto.ConfigureRequestArguments + await this.client.execute('configure', args) + this.cachedOption = options + this.requesting = false + } + + public async ensureConfigurationForDocument(document: TextDocument): Promise<void> { + let opts = await workspace.getFormatOptions(document.uri) + return this.ensureConfigurationOptions(document.languageId, opts.insertSpaces, opts.tabSize) + } + + public reset(): void { + this.cachedOption = null + } + + public getLanguageConfiguration(languageId: string): WorkspaceConfiguration { + return workspace.getConfiguration(languageId) + } + + public isTypeScriptDocument(languageId: string): boolean { + return languageId === languageIds.typescript || languageId === languageIds.typescriptreact || + languageId === languageIds.typescripttsx || languageId === languageIds.typescriptjsx + } + + public enableJavascript(): boolean { + const config = workspace.getConfiguration('tsserver') + return !!config.get<boolean>('enableJavascript') + } + + private getFileOptions(options: FormatOptions, languageId: string): FileConfiguration { + const lang = this.isTypeScriptDocument(languageId) ? 'typescript' : 'javascript' + return { + formatOptions: this.getFormatOptions(options, lang), + preferences: this.getPreferences(lang) + } + } + + private getFormatOptions(options: FormatOptions, language: string): Proto.FormatCodeSettings { + const config = workspace.getConfiguration(`${language}.format`) + + return { + tabSize: options.tabSize, + indentSize: options.tabSize, + convertTabsToSpaces: options.insertSpaces, + // We can use \n here since the editor normalizes later on to its line endings. + newLineCharacter: '\n', + insertSpaceAfterCommaDelimiter: config.get<boolean>('insertSpaceAfterCommaDelimiter'), + insertSpaceAfterConstructor: config.get<boolean>('insertSpaceAfterConstructor'), + insertSpaceAfterSemicolonInForStatements: config.get<boolean>('insertSpaceAfterSemicolonInForStatements'), + insertSpaceBeforeAndAfterBinaryOperators: config.get<boolean>('insertSpaceBeforeAndAfterBinaryOperators'), + insertSpaceAfterKeywordsInControlFlowStatements: config.get<boolean>('insertSpaceAfterKeywordsInControlFlowStatements'), + insertSpaceAfterFunctionKeywordForAnonymousFunctions: config.get<boolean>('insertSpaceAfterFunctionKeywordForAnonymousFunctions'), + insertSpaceBeforeFunctionParenthesis: config.get<boolean>('insertSpaceBeforeFunctionParenthesis'), + insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: config.get<boolean>('insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis'), + insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: config.get<boolean>('insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets'), + insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: config.get<boolean>('insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces'), + insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: config.get<boolean>('insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces'), + insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces: config.get<boolean>('insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces'), + insertSpaceAfterTypeAssertion: config.get<boolean>('insertSpaceAfterTypeAssertion'), + placeOpenBraceOnNewLineForFunctions: config.get<boolean>('placeOpenBraceOnNewLineForFunctions'), + placeOpenBraceOnNewLineForControlBlocks: config.get<boolean>('placeOpenBraceOnNewLineForControlBlocks') + } + } + + public getCompleteOptions(languageId: string): CompletionOptions { + const lang = this.isTypeScriptDocument(languageId) ? 'typescript' : 'javascript' + const config = workspace.getConfiguration(`${lang}.preferences.completion`) + return { + useCodeSnippetsOnMethodSuggest: config.get<boolean>('useCodeSnippetsOnMethodSuggest', true), + commaAfterImport: config.get<boolean>('commaAfterImport', true), + nameSuggestions: config.get<boolean>('nameSuggestions', true), + autoImportSuggestions: config.get<boolean>('autoImportSuggestions', true) + } + } + + public getPreferences(language: string): Proto.UserPreferences { + if (!this.client.apiVersion.gte(API.v290)) { + return {} + } + const config = workspace.getConfiguration(`${language}.preferences`) + return { + importModuleSpecifierPreference: getImportModuleSpecifier(config) as any, + disableSuggestions: !config.get<boolean>('suggestionActions.enabled', true), + quotePreference: getQuoteType(config), + includeCompletionsForModuleExports: config.get<boolean>('completion.moduleExports', true), + includeCompletionsWithInsertText: true, + allowTextChangesInNewFiles: false, + } + } +} + +type ModuleImportType = 'relative' | 'non-relative' | 'auto' +type QuoteType = 'single' | 'double' + +function getImportModuleSpecifier(config): ModuleImportType { + let val = config.get('importModuleSpecifier') + switch (val) { + case 'relative': + return 'relative' + case 'non-relative': + return 'non-relative' + default: + return 'auto' + } +} + +function getQuoteType(config): QuoteType { + let val = config.get('quoteStyle') + switch (val) { + case 'single': + return 'single' + case 'double': + return 'double' + default: + return 'single' + } +} diff --git a/src/server/features/folding.ts b/src/server/features/folding.ts new file mode 100644 index 0000000..ae73ba6 --- /dev/null +++ b/src/server/features/folding.ts @@ -0,0 +1,70 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from 'vscode-jsonrpc' +import { FoldingRange, TextDocument } from 'vscode-languageserver-types' +import { FoldingContext, FoldingRangeProvider } from 'coc.nvim/lib/provider' +import { workspace } from 'coc.nvim' +import Proto from '../protocol' +import { ITypeScriptServiceClient } from '../typescriptService' +import * as typeConverters from '../utils/typeConverters' + +export default class TypeScriptFoldingProvider implements FoldingRangeProvider { + public constructor(private readonly client: ITypeScriptServiceClient) { } + + public async provideFoldingRanges( + document: TextDocument, + _context: FoldingContext, + token: CancellationToken + ): Promise<FoldingRange[] | undefined> { + const file = this.client.toPath(document.uri) + if (!file) { + return + } + + const args: Proto.FileRequestArgs = { file } + const { body } = await this.client.execute('getOutliningSpans', args, token) + if (!body) { + return + } + + return body + .map(span => this.convertOutliningSpan(span, document)) + .filter(foldingRange => !!foldingRange) as FoldingRange[] + } + + private convertOutliningSpan( + span: Proto.OutliningSpan, + document: TextDocument + ): FoldingRange | undefined { + const range = typeConverters.Range.fromTextSpan(span.textSpan) + const kind = TypeScriptFoldingProvider.getFoldingRangeKind(span) + + // Workaround for #49904 + if (span.kind === 'comment') { + let doc = workspace.getDocument(document.uri) + const line = doc.getline(range.start.line) + if (line.match(/\/\/\s*#endregion/gi)) { + return undefined + } + } + let { start, end } = range + return FoldingRange.create(start.line, end.line, start.character, end.character, kind) + } + + private static getFoldingRangeKind( + span: Proto.OutliningSpan + ): string { + switch (span.kind) { + case 'comment': + case 'region': + case 'imports': + case 'code': + return span.kind + default: + return undefined + } + } +} diff --git a/src/server/features/formatting.ts b/src/server/features/formatting.ts new file mode 100644 index 0000000..48bd388 --- /dev/null +++ b/src/server/features/formatting.ts @@ -0,0 +1,158 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { CancellationToken, FormattingOptions, Position, Range, TextDocument, TextEdit } from 'vscode-languageserver-protocol' +import { commands, workspace } from 'coc.nvim' +import { DocumentFormattingEditProvider, DocumentRangeFormattingEditProvider } from 'coc.nvim/lib/provider' +import * as Proto from '../protocol' +import { ITypeScriptServiceClient } from '../typescriptService' +import { languageIds } from '../utils/languageModeIds' +import * as typeConverters from '../utils/typeConverters' +import FileConfigurationManager from './fileConfigurationManager' + +export default class TypeScriptFormattingProvider + implements + DocumentRangeFormattingEditProvider, + DocumentFormattingEditProvider { + public constructor( + private readonly client: ITypeScriptServiceClient, + private readonly formattingOptionsManager: FileConfigurationManager + ) { + commands.register({ + id: 'tsserver.format', + execute: async (): Promise<void> => { + let document = await workspace.document + if (!document) return + if (languageIds.indexOf(document.filetype) == -1) { + return + } + let options = await workspace.getFormatOptions() + let edit = await this.provideDocumentFormattingEdits( + document.textDocument, + options + ) + if (!edit) return + await document.applyEdits(workspace.nvim, edit) + } + }) + } + + private async doFormat( + document: TextDocument, + options: FormattingOptions, + args: Proto.FormatRequestArgs, + token?: CancellationToken + ): Promise<TextEdit[]> { + await this.formattingOptionsManager.ensureConfigurationOptions( + document.languageId, + options.insertSpaces, + options.tabSize + ) + try { + const response = await this.client.execute('format', args, token) + if (response.body) { + return response.body.map(typeConverters.TextEdit.fromCodeEdit) + } + } catch { + // noop + } + return [] + } + + public async provideDocumentRangeFormattingEdits( + document: TextDocument, + range: Range, + options: FormattingOptions, + token: CancellationToken + ): Promise<TextEdit[]> { + const filepath = this.client.toPath(document.uri) + if (!filepath) return [] + const args: Proto.FormatRequestArgs = { + file: filepath, + line: range.start.line + 1, + offset: range.start.character + 1, + endLine: range.end.line + 1, + endOffset: range.end.character + 1 + } + return this.doFormat(document, options, args, token) + } + + public async provideDocumentFormattingEdits( + document: TextDocument, + options: FormattingOptions, + token?: CancellationToken + ): Promise<TextEdit[]> { + const filepath = this.client.toPath(document.uri) + if (!filepath) return [] + const args: Proto.FormatRequestArgs = { + file: filepath, + line: 1, + offset: 1, + endLine: document.lineCount + 1, + endOffset: 1 + } + return this.doFormat(document, options, args, token) + } + + public async provideOnTypeFormattingEdits( + document: TextDocument, + position: Position, + ch: string, + options: FormattingOptions, + token: CancellationToken + ): Promise<TextEdit[]> { + if (!this.client.configuration.formatOnType) { + return + } + const file = this.client.toPath(document.uri) + if (!file) { + return [] + } + + await this.formattingOptionsManager.ensureConfigurationOptions( + document.languageId, + options.insertSpaces, + options.tabSize + ) + const doc = workspace.getDocument(document.uri) + + const args: Proto.FormatOnKeyRequestArgs = { + ...typeConverters.Position.toFileLocationRequestArgs(file, position), + key: ch + } + try { + const { body } = await this.client.execute('formatonkey', args, token) + const edits = body + const result: TextEdit[] = [] + if (!edits) { + return result + } + for (const edit of edits) { + const textEdit = typeConverters.TextEdit.fromCodeEdit(edit) + const range = textEdit.range + // Work around for https://github.com/Microsoft/TypeScript/issues/6700. + // Check if we have an edit at the beginning of the line which only removes white spaces and leaves + // an empty line. Drop those edits + if ( + range.start.character === 0 && + range.start.line === range.end.line && + textEdit.newText === '' + ) { + const lText = doc.getline(range.start.line) + // If the edit leaves something on the line keep the edit (note that the end character is exclusive). + // Keep it also if it removes something else than whitespace + if (lText.trim().length > 0 || lText.length > range.end.character) { + result.push(textEdit) + } + } else { + result.push(textEdit) + } + } + return result + } catch { + // noop + } + return [] + } +} diff --git a/src/server/features/hover.ts b/src/server/features/hover.ts new file mode 100644 index 0000000..09f3ea1 --- /dev/null +++ b/src/server/features/hover.ts @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { CancellationToken, Hover, MarkedString, Position, TextDocument } from 'vscode-languageserver-protocol' +import { HoverProvider } from 'coc.nvim/lib/provider' +import * as Proto from '../protocol' +import { ITypeScriptServiceClient } from '../typescriptService' +import { tagsMarkdownPreview } from '../utils/previewer' +import * as typeConverters from '../utils/typeConverters' + +export default class TypeScriptHoverProvider implements HoverProvider { + public constructor(private readonly client: ITypeScriptServiceClient) { } + + public async provideHover( + document: TextDocument, + position: Position, + token: CancellationToken + ): Promise<Hover | undefined> { + const filepath = this.client.toPath(document.uri) + if (!filepath) { + return undefined + } + const args = typeConverters.Position.toFileLocationRequestArgs( + filepath, + position + ) + try { + const response = await this.client.execute('quickinfo', args, token) + if (response && response.body) { + const data = response.body + return { + contents: TypeScriptHoverProvider.getContents(data), + range: typeConverters.Range.fromTextSpan(data) + } + } + } catch (e) { + // noop + } + return undefined + } + + private static getContents(data: Proto.QuickInfoResponseBody): MarkedString[] { // tslint:disable-line + const parts = [] + + if (data.displayString) { + parts.push({ language: 'typescript', value: data.displayString }) + } + + const tags = tagsMarkdownPreview(data.tags) + parts.push(data.documentation + (tags ? '\n\n' + tags : '')) + return parts + } +} diff --git a/src/server/features/implementationsCodeLens.ts b/src/server/features/implementationsCodeLens.ts new file mode 100644 index 0000000..56398ef --- /dev/null +++ b/src/server/features/implementationsCodeLens.ts @@ -0,0 +1,100 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { CancellationToken, CodeLens, Command, Location, Range, TextDocument } from 'vscode-languageserver-protocol' +import * as Proto from '../protocol' +import * as PConst from '../protocol.const' +import * as typeConverters from '../utils/typeConverters' +import { TypeScriptBaseCodeLensProvider } from './baseCodeLensProvider' + +export default class TypeScriptImplementationsCodeLensProvider extends TypeScriptBaseCodeLensProvider { + public async resolveCodeLens( + codeLens: CodeLens, + token: CancellationToken + ): Promise<CodeLens> { + let { uri } = codeLens.data + let filepath = this.client.toPath(uri) + + const args = typeConverters.Position.toFileLocationRequestArgs( + filepath, + codeLens.range.start + ) + try { + const response = await this.client.execute('implementation', args, token) + if (response && response.body) { + const locations = response.body + .map(reference => { + return { + uri: this.client.toResource(reference.file), + range: { + start: typeConverters.Position.fromLocation(reference.start), + end: { + line: reference.start.line, + character: 0 + } + } + } + }) + // Exclude original from implementations + .filter( + location => !( + location.uri.toString() === uri && + location.range.start.line === codeLens.range.start.line && + location.range.start.character === + codeLens.range.start.character + ) + ) + + codeLens.command = this.getCommand(locations, codeLens) + return codeLens + } + } catch { + // noop + } + + codeLens.command = { + title: 'Could not determine implementations', + command: '' + } + return codeLens + } + + private getCommand( + locations: Location[], + codeLens: CodeLens, + ): Command | undefined { + let { uri } = codeLens.data + return { + title: this.getTitle(locations), + command: locations.length ? 'editor.action.showReferences' : '', + arguments: [uri, codeLens.range.start, locations] + } + } + + private getTitle(locations: Location[]): string { + return locations.length === 1 ? '1 implementation' : `${locations.length} implementations` + } + + protected extractSymbol( + document: TextDocument, + item: Proto.NavigationTree, + _parent: Proto.NavigationTree | null + ): Range | null { + switch (item.kind) { + case PConst.Kind.interface: + return super.getSymbolRange(document, item) + + case PConst.Kind.class: + case PConst.Kind.memberFunction: + case PConst.Kind.memberVariable: + case PConst.Kind.memberGetAccessor: + case PConst.Kind.memberSetAccessor: + if (item.kindModifiers.match(/\babstract\b/g)) { + return super.getSymbolRange(document, item) + } + break + } + return null + } +} diff --git a/src/server/features/organizeImports.ts b/src/server/features/organizeImports.ts new file mode 100644 index 0000000..0e4568d --- /dev/null +++ b/src/server/features/organizeImports.ts @@ -0,0 +1,101 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { disposeAll, TextDocumentWillSaveEvent, workspace } from 'coc.nvim' +import { Command, CommandManager } from 'coc.nvim/lib/commands' +import { Disposable, TextDocument, TextEdit, WorkspaceEdit } from 'vscode-languageserver-protocol' +import Proto from '../protocol' +import { ITypeScriptServiceClient } from '../typescriptService' +import { standardLanguageDescriptions } from '../utils/languageDescription' +import * as typeconverts from '../utils/typeConverters' +import FileConfigurationManager from './fileConfigurationManager' + +class OrganizeImportsCommand implements Command { + public readonly id: string = 'tsserver.organizeImports' + + constructor( + private readonly client: ITypeScriptServiceClient, + private commaAfterImport: boolean, + private modeIds: string[] + ) { + workspace.onWillSaveUntil(this.onWillSaveUntil, this, 'tsserver-organizeImports') + } + + private onWillSaveUntil(event: TextDocumentWillSaveEvent): void { + let config = workspace.getConfiguration('tsserver') + let format = config.get('orgnizeImportOnSave', false) + if (!format) return + let { document } = event + if (this.modeIds.indexOf(document.languageId) == -1) return + let willSaveWaitUntil = async (): Promise<TextEdit[]> => { + let edit = await this.getTextEdits(document) + if (!edit) return [] + return edit.changes ? edit.changes[document.uri] : [] + } + event.waitUntil(willSaveWaitUntil()) + } + + private async getTextEdits(document: TextDocument): Promise<WorkspaceEdit | null> { + let file = this.client.toPath(document.uri) + const args: Proto.OrganizeImportsRequestArgs = { + scope: { + type: 'file', + args: { + file + } + } + } + const response = await this.client.execute('organizeImports', args) + if (!response || !response.success) { + return + } + + const edit = typeconverts.WorkspaceEdit.fromFileCodeEdits( + this.client, + response.body + ) + if (!this.commaAfterImport) { + let { changes } = edit + if (changes) { + for (let c of Object.keys(changes)) { + for (let textEdit of changes[c]) { + textEdit.newText = textEdit.newText.replace(/;/g, '') + } + } + } + } + return edit + } + + public async execute(): Promise<void> { + let document = await workspace.document + if (this.modeIds.indexOf(document.filetype) == -1) return + let edit = await this.getTextEdits(document.textDocument) + if (edit) await workspace.applyEdit(edit) + return + } +} + +export default class OrganizeImports { + private disposables: Disposable[] = [] + public constructor( + client: ITypeScriptServiceClient, + commandManager: CommandManager, + fileConfigurationManager: FileConfigurationManager, + languageId: string + ) { + let description = standardLanguageDescriptions.find(o => o.id == languageId) + let modeIds = description ? description.modeIds : [] + let option = fileConfigurationManager.getCompleteOptions(languageId) + let cmd = new OrganizeImportsCommand(client, option.commaAfterImport, modeIds) + commandManager.register(cmd) + this.disposables.push(Disposable.create(() => { + commandManager.unregister(cmd.id) + })) + } + + public dispose(): void { + disposeAll(this.disposables) + } +} diff --git a/src/server/features/projectError.ts b/src/server/features/projectError.ts new file mode 100644 index 0000000..c62132b --- /dev/null +++ b/src/server/features/projectError.ts @@ -0,0 +1,49 @@ +import { disposeAll, workspace } from 'coc.nvim' +import { Command, CommandManager } from 'coc.nvim/lib/commands' +import { Disposable } from 'vscode-languageserver-protocol' +import * as Proto from '../protocol' +import { ITypeScriptServiceClient } from '../typescriptService' +import * as languageIds from '../utils/languageModeIds' + +class ProjectErrorCommand implements Command { + public readonly id: string = 'tsserver.project_error' + + constructor( + private readonly client: ITypeScriptServiceClient + ) { + } + + public async execute(): Promise<void> { + let document = await workspace.document + if (languageIds[document.filetype] == null) return + let file = this.client.toPath(document.uri) + const args: Proto.GeterrForProjectRequestArgs = { + file, + delay: 20 + } + const response = await this.client.execute('geterrForProject', args) + if (!response || !response.success) { + return + } + + return + } +} + +export default class ProjectErrors { + private disposables: Disposable[] = [] + public constructor( + client: ITypeScriptServiceClient, + commandManager: CommandManager + ) { + let cmd = new ProjectErrorCommand(client) + commandManager.register(cmd) + this.disposables.push(Disposable.create(() => { + commandManager.unregister(cmd.id) + })) + } + + public dispose(): void { + disposeAll(this.disposables) + } +} diff --git a/src/server/features/quickfix.ts b/src/server/features/quickfix.ts new file mode 100644 index 0000000..85b6185 --- /dev/null +++ b/src/server/features/quickfix.ts @@ -0,0 +1,284 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { commands, workspace } from 'coc.nvim' +import { Command } from 'coc.nvim/lib/commands' +import { CodeActionProvider } from 'coc.nvim/lib/provider' +import { CancellationToken, CodeAction, CodeActionContext, CodeActionKind, Diagnostic, Range, TextDocument } from 'vscode-languageserver-protocol' +import * as Proto from '../protocol' +import { ITypeScriptServiceClient } from '../typescriptService' +import API from '../utils/api' +import { applyCodeActionCommands, getEditForCodeAction } from '../utils/codeAction' +import * as typeConverters from '../utils/typeConverters' +import BufferSyncSupport from './bufferSyncSupport' +import { DiagnosticsManager } from './diagnostics' + +class ApplyCodeActionCommand implements Command { + public static readonly ID = '_typescript.applyCodeActionCommand' + public readonly id = ApplyCodeActionCommand.ID + + constructor( + private readonly client: ITypeScriptServiceClient, + ) { } + + public async execute(action: Proto.CodeFixAction): Promise<boolean> { + return applyCodeActionCommands(this.client, action) + } +} + +class ApplyFixAllCodeAction implements Command { + public static readonly ID = '_typescript.applyFixAllCodeAction' + public readonly id = ApplyFixAllCodeAction.ID + + constructor( + private readonly client: ITypeScriptServiceClient, + ) { } + + public async execute( + file: string, + tsAction: Proto.CodeFixAction + ): Promise<void> { + if (!tsAction.fixId) { + return + } + + const args: Proto.GetCombinedCodeFixRequestArgs = { + scope: { + type: 'file', + args: { file } + }, + fixId: tsAction.fixId + } + + try { + const { body } = await this.client.execute('getCombinedCodeFix', args) + if (!body) { + return + } + + const edit = typeConverters.WorkspaceEdit.fromFileCodeEdits( + this.client, + body.changes + ) + await workspace.applyEdit(edit) + const token = CancellationToken.None + + const { commands } = body + if (commands && commands.length) { + for (const command of commands) { + await this.client.execute('applyCodeActionCommand', { command }, token) + } + } + } catch { + // noop + } + } +} + +/** + * Unique set of diagnostics keyed on diagnostic range and error code. + */ +class DiagnosticsSet { + public static from(diagnostics: Diagnostic[]): DiagnosticsSet { + const values = new Map<string, Diagnostic>() + for (const diagnostic of diagnostics) { + values.set(DiagnosticsSet.key(diagnostic), diagnostic) + } + return new DiagnosticsSet(values) + } + + private static key(diagnostic: Diagnostic): string { + const { start, end } = diagnostic.range + return `${diagnostic.code}-${start.line},${start.character}-${end.line},${end.character}` + } + + private constructor( + private readonly _values: Map<string, Diagnostic> + ) { } + + public get values(): Iterable<Diagnostic> { + return this._values.values() + } +} + +class SupportedCodeActionProvider { + private _supportedCodeActions?: Thenable<Set<number>> + + public constructor(private readonly client: ITypeScriptServiceClient) { } + + public async getFixableDiagnosticsForContext( + context: CodeActionContext + ): Promise<Diagnostic[]> { + const supportedActions = await this.supportedCodeActions + const fixableDiagnostics = DiagnosticsSet.from( + context.diagnostics.filter(diagnostic => + supportedActions.has(+diagnostic.code!) + ) + ) + return Array.from(fixableDiagnostics.values) + } + + private get supportedCodeActions(): Promise<Set<number>> { + if (!this._supportedCodeActions) { + this._supportedCodeActions = this.client + .execute('getSupportedCodeFixes', null, undefined) + .then(response => response.body || []) + .then(codes => codes.map(code => +code).filter(code => !isNaN(code))) + .then(codes => new Set(codes)) + } + return Promise.resolve(this._supportedCodeActions) + } +} + +export default class TypeScriptQuickFixProvider implements CodeActionProvider { + private readonly supportedCodeActionProvider: SupportedCodeActionProvider + + constructor( + private readonly client: ITypeScriptServiceClient, + private readonly diagnosticsManager: DiagnosticsManager, + private readonly bufferSyncSupport: BufferSyncSupport, + ) { + commands.register( + new ApplyCodeActionCommand(client) + ) + commands.register( + new ApplyFixAllCodeAction(client) + ) + + this.supportedCodeActionProvider = new SupportedCodeActionProvider(client) + } + + public async provideCodeActions( + document: TextDocument, + _range: Range, + context: CodeActionContext, + token: CancellationToken + ): Promise<CodeAction[]> { + if (!this.client.apiVersion.gte(API.v213)) { + return [] + } + + const file = this.client.toPath(document.uri) + if (!file) { + return [] + } + + const fixableDiagnostics = await this.supportedCodeActionProvider.getFixableDiagnosticsForContext( + context + ) + if (!fixableDiagnostics.length) { + return [] + } + + if (this.bufferSyncSupport.hasPendingDiagnostics(document.uri)) { + return [] + } + + const results: CodeAction[] = [] + for (const diagnostic of fixableDiagnostics) { + results.push( + ...(await this.getFixesForDiagnostic(document, file, diagnostic, token)) + ) + } + return results + } + + private async getFixesForDiagnostic( + document: TextDocument, + file: string, + diagnostic: Diagnostic, + token: CancellationToken + ): Promise<Iterable<CodeAction>> { + const args: Proto.CodeFixRequestArgs = { + ...typeConverters.Range.toFileRangeRequestArgs(file, diagnostic.range), + errorCodes: [+diagnostic.code!] + } + const codeFixesResponse = await this.client.execute( + 'getCodeFixes', + args, + token + ) + if (codeFixesResponse.body) { + const results: CodeAction[] = [] + for (const tsCodeFix of codeFixesResponse.body) { + results.push( + ...(await this.getAllFixesForTsCodeAction( + document, + file, + diagnostic, + tsCodeFix + )) + ) + } + return results + } + return [] + } + + private async getAllFixesForTsCodeAction( + document: TextDocument, + file: string, + diagnostic: Diagnostic, + tsAction: Proto.CodeAction + ): Promise<Iterable<CodeAction>> { + const singleFix = this.getSingleFixForTsCodeAction(diagnostic, tsAction) + const fixAll = await this.getFixAllForTsCodeAction( + document, + file, + diagnostic, + tsAction as Proto.CodeFixAction + ) + return fixAll ? [singleFix, fixAll] : [singleFix] + } + + private getSingleFixForTsCodeAction( + diagnostic: Diagnostic, + tsAction: Proto.CodeAction + ): CodeAction { + const codeAction: CodeAction = { + title: tsAction.description, + kind: CodeActionKind.QuickFix + } + codeAction.edit = getEditForCodeAction(this.client, tsAction) + codeAction.diagnostics = [diagnostic] + if (tsAction.commands) { + codeAction.command = { + command: ApplyCodeActionCommand.ID, + arguments: [tsAction], + title: tsAction.description + } + } + return codeAction + } + + private async getFixAllForTsCodeAction( + document: TextDocument, + file: string, + diagnostic: Diagnostic, + tsAction: Proto.CodeFixAction + ): Promise<CodeAction | undefined> { + if (!tsAction.fixId || !this.client.apiVersion.gte(API.v270)) { + return undefined + } + + // Make sure there are multiple diagnostics of the same type in the file + if (!this.diagnosticsManager + .getDiagnostics(document.uri) + .some(x => x.code === diagnostic.code && x !== diagnostic)) { + return + } + + const action: CodeAction = { + title: tsAction.fixAllDescription || 'Fix all in file', + kind: CodeActionKind.QuickFix + } + action.diagnostics = [diagnostic] + action.command = { + command: ApplyFixAllCodeAction.ID, + arguments: [file, tsAction], + title: '' + } + return action + } +} diff --git a/src/server/features/refactor.ts b/src/server/features/refactor.ts new file mode 100644 index 0000000..b452315 --- /dev/null +++ b/src/server/features/refactor.ts @@ -0,0 +1,221 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { CancellationToken, CodeAction, CodeActionContext, CodeActionKind, Range, TextDocument, WorkspaceEdit } from 'vscode-languageserver-protocol' +import { Command } from 'coc.nvim/lib/commands' +import { CodeActionProvider, CodeActionProviderMetadata } from 'coc.nvim/lib/provider' +import { workspace, commands } from 'coc.nvim' +import Proto from '../protocol' +import { ITypeScriptServiceClient } from '../typescriptService' +import * as typeConverters from '../utils/typeConverters' +import FormattingOptionsManager from './fileConfigurationManager' + +class ApplyRefactoringCommand implements Command { + public static readonly ID = '_typescript.applyRefactoring' + public readonly id = ApplyRefactoringCommand.ID + + constructor(private readonly client: ITypeScriptServiceClient) { } + + public async execute( + document: TextDocument, + file: string, + refactor: string, + action: string, + range: Range + ): Promise<boolean> { + const args: Proto.GetEditsForRefactorRequestArgs = { + ...typeConverters.Range.toFileRangeRequestArgs(file, range), + refactor, + action + } + const response = await this.client.execute('getEditsForRefactor', args) + const body = response && response.body + if (!body || !body.edits.length) { + return false + } + + const workspaceEdit = await this.toWorkspaceEdit(body) + if (!(await workspace.applyEdit(workspaceEdit))) { + return false + } + const renameLocation = body.renameLocation + if (renameLocation) { + commands.executeCommand('editor.action.rename', + document.uri, + typeConverters.Position.fromLocation(renameLocation) + ) + } + return true + } + + private async toWorkspaceEdit(body: Proto.RefactorEditInfo): Promise<WorkspaceEdit> { + for (const edit of body.edits) { + await workspace.createFile(edit.fileName, { ignoreIfExists: true }) + } + let workspaceEdit = typeConverters.WorkspaceEdit.fromFileCodeEdits( + this.client, + body.edits + ) + return workspaceEdit + } +} + +class SelectRefactorCommand implements Command { + public static readonly ID = '_typescript.selectRefactoring' + public readonly id = SelectRefactorCommand.ID + + constructor(private readonly doRefactoring: ApplyRefactoringCommand) { } + + public async execute( + document: TextDocument, + file: string, + info: Proto.ApplicableRefactorInfo, + range: Range + ): Promise<boolean> { + let { actions } = info + const idx = actions.length == 1 ? 0 : await workspace.showQuickpick( + actions.map(action => action.description || action.name) + ) + if (idx == -1) return false + let label = info.actions[idx].name + if (!label) return false + return this.doRefactoring.execute( + document, + file, + info.name, + label, + range + ) + } +} + +export default class TypeScriptRefactorProvider implements CodeActionProvider { + private static readonly extractFunctionKind = CodeActionKind.RefactorExtract + '.function' + private static readonly extractConstantKind = CodeActionKind.RefactorExtract + '.constant' + private static readonly moveKind = CodeActionKind.Refactor + '.move' + + constructor( + private readonly client: ITypeScriptServiceClient, + private readonly formattingOptionsManager: FormattingOptionsManager, + ) { + const doRefactoringCommand = commands.register( + new ApplyRefactoringCommand(this.client) + ) + commands.register(new SelectRefactorCommand(doRefactoringCommand)) + } + + public static readonly metadata: CodeActionProviderMetadata = { + providedCodeActionKinds: [CodeActionKind.Refactor] + } + + public async provideCodeActions( + document: TextDocument, + range: Range, + context: CodeActionContext, + token: CancellationToken + ): Promise<CodeAction[] | undefined> { + if (!this.shouldTrigger(context)) { + return undefined + } + const file = this.client.toPath(document.uri) + if (!file) return undefined + await this.formattingOptionsManager.ensureConfigurationForDocument(document) + const args: Proto.GetApplicableRefactorsRequestArgs = typeConverters.Range.toFileRangeRequestArgs( + file, + range + ) + let response: Proto.GetApplicableRefactorsResponse + try { + response = await this.client.execute('getApplicableRefactors', args, token) + if (!response || !response.body) { + return undefined + } + } catch { + return undefined + } + + return this.convertApplicableRefactors( + response.body, + document, + file, + range + ) + } + + private convertApplicableRefactors( + body: Proto.ApplicableRefactorInfo[], + document: TextDocument, + file: string, + rangeOrSelection: Range + ): CodeAction[] { + const actions: CodeAction[] = [] + for (const info of body) { + if (!info.inlineable) { + const codeAction: CodeAction = { + title: info.description, + kind: CodeActionKind.Refactor + } + codeAction.command = { + title: info.description, + command: SelectRefactorCommand.ID, + arguments: [document, file, info, rangeOrSelection] + } + actions.push(codeAction) + } else { + for (const action of info.actions) { + actions.push( + this.refactorActionToCodeAction( + action, + document, + file, + info, + rangeOrSelection + ) + ) + } + } + } + return actions + } + + private refactorActionToCodeAction( + action: Proto.RefactorActionInfo, + document: TextDocument, + file: string, + info: Proto.ApplicableRefactorInfo, + rangeOrSelection: Range + ): CodeAction { + const codeAction: CodeAction = { + title: action.description, + kind: TypeScriptRefactorProvider.getKind(action) + } + codeAction.command = { + title: action.description, + command: ApplyRefactoringCommand.ID, + arguments: [document, file, info.name, action.name, rangeOrSelection] + } + return codeAction + } + + private shouldTrigger(context: CodeActionContext): boolean { + if ( + context.only && + context.only.indexOf(CodeActionKind.Refactor) == -1 + ) { + return false + } + return true + } + + private static getKind(refactor: Proto.RefactorActionInfo): string { + if (refactor.name.startsWith('function_')) { + return TypeScriptRefactorProvider.extractFunctionKind + } else if (refactor.name.startsWith('constant_')) { + return TypeScriptRefactorProvider.extractConstantKind + } else if (refactor.name.startsWith('Move')) { + return TypeScriptRefactorProvider.moveKind + } + return CodeActionKind.Refactor + } +} diff --git a/src/server/features/references.ts b/src/server/features/references.ts new file mode 100644 index 0000000..40200c1 --- /dev/null +++ b/src/server/features/references.ts @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { CancellationToken, Location, Position, TextDocument } from 'vscode-languageserver-protocol' +import { ReferenceContext, ReferenceProvider } from 'coc.nvim/lib/provider' +import { ITypeScriptServiceClient } from '../typescriptService' +import * as typeConverters from '../utils/typeConverters' + +export default class TypeScriptReferences implements ReferenceProvider { + public constructor(private readonly client: ITypeScriptServiceClient) { + } + + public async provideReferences( + document: TextDocument, + position: Position, + context: ReferenceContext, + token: CancellationToken + ): Promise<Location[]> { + const filepath = this.client.toPath(document.uri) + if (!filepath) return [] + + const args = typeConverters.Position.toFileLocationRequestArgs( + filepath, + position + ) + try { + const msg = await this.client.execute('references', args, token) + if (!msg.body) { + return [] + } + const result: Location[] = [] + for (const ref of msg.body.refs) { + if (!context.includeDeclaration && ref.isDefinition) { + continue + } + const url = this.client.toResource(ref.file) + const location = typeConverters.Location.fromTextSpan(url, ref) + result.push(location) + } + return result + } catch { + return [] + } + } +} diff --git a/src/server/features/referencesCodeLens.ts b/src/server/features/referencesCodeLens.ts new file mode 100644 index 0000000..e7549df --- /dev/null +++ b/src/server/features/referencesCodeLens.ts @@ -0,0 +1,102 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { CancellationToken, CodeLens, Range, TextDocument } from 'vscode-languageserver-protocol' +import * as Proto from '../protocol' +import * as PConst from '../protocol.const' +import * as typeConverters from '../utils/typeConverters' +import { TypeScriptBaseCodeLensProvider } from './baseCodeLensProvider' + +export default class TypeScriptReferencesCodeLensProvider extends TypeScriptBaseCodeLensProvider { + public resolveCodeLens( + codeLens: CodeLens, + token: CancellationToken + ): Promise<CodeLens> { + let { uri } = codeLens.data + let filepath = this.client.toPath(uri) + const args = typeConverters.Position.toFileLocationRequestArgs( + filepath, + codeLens.range.start + ) + return this.client + .execute('references', args, token) + .then(response => { + if (!response || !response.body) { + throw codeLens + } + + const locations = response.body.refs + .map(reference => + typeConverters.Location.fromTextSpan( + this.client.toResource(reference.file), + reference + ) + ) + .filter( + location => + // Exclude original definition from references + !( + location.uri.toString() === uri && + location.range.start.line === codeLens.range.start.line && + location.range.start.character === + codeLens.range.start.character + ) + ) + + codeLens.command = { + title: locations.length === 1 ? '1 reference' : `${locations.length} references`, + command: locations.length ? 'editor.action.showReferences' : '', + arguments: [uri, codeLens.range.start, locations] + } + return codeLens + }) + .catch(() => { + codeLens.command = { + title: 'Could not determine references', + command: '' + } + return codeLens + }) + } + + protected extractSymbol( + document: TextDocument, + item: Proto.NavigationTree, + parent: Proto.NavigationTree | null + ): Range | null { + if (parent && parent.kind === PConst.Kind.enum) { + return super.getSymbolRange(document, item) + } + + switch (item.kind) { + case PConst.Kind.const: + case PConst.Kind.let: + case PConst.Kind.variable: + case PConst.Kind.function: + // Only show references for exported variables + if (!item.kindModifiers.match(/\bexport\b/)) { + break + } + // fallthrough + + case PConst.Kind.class: + if (item.text === '<class>') { + break + } + // fallthrough + + case PConst.Kind.memberFunction: + case PConst.Kind.memberVariable: + case PConst.Kind.memberGetAccessor: + case PConst.Kind.memberSetAccessor: + case PConst.Kind.constructorImplementation: + case PConst.Kind.interface: + case PConst.Kind.type: + case PConst.Kind.enum: + return super.getSymbolRange(document, item) + } + + return null + } +} diff --git a/src/server/features/rename.ts b/src/server/features/rename.ts new file mode 100644 index 0000000..de51af2 --- /dev/null +++ b/src/server/features/rename.ts @@ -0,0 +1,70 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { CancellationToken, Position, TextDocument, TextEdit, WorkspaceEdit } from 'vscode-languageserver-protocol' +import { RenameProvider } from 'coc.nvim/lib/provider' +import * as Proto from '../protocol' +import { ITypeScriptServiceClient } from '../typescriptService' +import * as typeConverters from '../utils/typeConverters' + +export default class TypeScriptRenameProvider implements RenameProvider { + public constructor(private readonly client: ITypeScriptServiceClient) { } + + public async provideRenameEdits( + document: TextDocument, + position: Position, + newName: string, + token: CancellationToken + ): Promise<WorkspaceEdit | null> { + const file = this.client.toPath(document.uri) + if (!file) { + return null + } + + const args: Proto.RenameRequestArgs = { + ...typeConverters.Position.toFileLocationRequestArgs(file, position), + findInStrings: false, + findInComments: false + } + + try { + const response = await this.client.execute('rename', args, token) + if (!response.body) { + return null + } + + const renameInfo = response.body.info + if (!renameInfo.canRename) { + return Promise.reject<WorkspaceEdit>( + renameInfo.localizedErrorMessage + ) + } + + return this.toWorkspaceEdit(response.body.locs, newName) + } catch { + // noop + } + return null + } + + private toWorkspaceEdit( + locations: ReadonlyArray<Proto.SpanGroup>, + newName: string + ): WorkspaceEdit { + let changes: { [uri: string]: TextEdit[] } = {} + for (const spanGroup of locations) { + const uri = this.client.toResource(spanGroup.file) + if (uri) { + changes[uri] = [] + for (const textSpan of spanGroup.locs) { + changes[uri].push({ + range: typeConverters.Range.fromTextSpan(textSpan), + newText: newName + }) + } + } + } + return { changes } + } +} diff --git a/src/server/features/resourceMap.ts b/src/server/features/resourceMap.ts new file mode 100644 index 0000000..ad71fb3 --- /dev/null +++ b/src/server/features/resourceMap.ts @@ -0,0 +1,81 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Maps of file resources + * + * Attempts to handle correct mapping on both case sensitive and case in-sensitive + * file systems. + */ +export class ResourceMap<T> { + private readonly _map = new Map<string, T>() + + constructor( + private readonly _normalizePath?: (resource: string) => string | null + ) { } + + public has(resource: string): boolean { + const file = this.toKey(resource) + return !!file && this._map.has(file) + } + + public get(resource: string): T | undefined { + const file = this.toKey(resource) + return file ? this._map.get(file) : undefined + } + + public set(resource: string, value: T): void { + const file = this.toKey(resource) + if (file) { + this._map.set(file, value) + } + } + + public delete(resource: string): void { + const file = this.toKey(resource) + if (file) { + this._map.delete(file) + } + } + + public get values(): Iterable<T> { + return this._map.values() + } + + public get keys(): Iterable<string> { + return this._map.keys() + } + + private toKey(resource: string): string | null { + const key = this._normalizePath + ? this._normalizePath(resource) + : resource + if (!key) { + return key + } + return this.isCaseInsensitivePath(key) ? key.toLowerCase() : key + } + + private isCaseInsensitivePath(path: string): boolean { + if (isWindowsPath(path)) { + return true + } + return path[0] === '/' && this.onIsCaseInsenitiveFileSystem + } + + private get onIsCaseInsenitiveFileSystem(): boolean { + if (process.platform === 'win32') { + return true + } + if (process.platform === 'darwin') { + return true + } + return false + } +} + +export function isWindowsPath(path: string): boolean { + return /^[a-zA-Z]:\\/.test(path) +} diff --git a/src/server/features/signatureHelp.ts b/src/server/features/signatureHelp.ts new file mode 100644 index 0000000..2a31bf4 --- /dev/null +++ b/src/server/features/signatureHelp.ts @@ -0,0 +1,73 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { CancellationToken, Position, SignatureHelp, SignatureInformation, TextDocument } from 'vscode-languageserver-protocol' +import { SignatureHelpProvider } from 'coc.nvim/lib/provider' +import * as Proto from '../protocol' +import { ITypeScriptServiceClient } from '../typescriptService' +import * as Previewer from '../utils/previewer' +import * as typeConverters from '../utils/typeConverters' + +export default class TypeScriptSignatureHelpProvider implements SignatureHelpProvider { + public static readonly triggerCharacters = ['(', ',', '<'] + + public constructor(private readonly client: ITypeScriptServiceClient) { } + + public async provideSignatureHelp( + document: TextDocument, + position: Position, + token: CancellationToken + ): Promise<SignatureHelp | undefined> { + const filepath = this.client.toPath(document.uri) + if (!filepath) { + return undefined + } + const args: Proto.SignatureHelpRequestArgs = typeConverters.Position.toFileLocationRequestArgs( + filepath, + position + ) + + let info: Proto.SignatureHelpItems | undefined + try { + const response = await this.client.execute('signatureHelp', args, token) + info = response.body + if (!info) return undefined + } catch { + return undefined + } + + const result: SignatureHelp = { + activeSignature: info.selectedItemIndex, + activeParameter: this.getActiveParmeter(info), + signatures: info.items.map(signature => { + return this.convertSignature(signature) + }) + } + return result + } + + private getActiveParmeter(info: Proto.SignatureHelpItems): number { + const activeSignature = info.items[info.selectedItemIndex] + if (activeSignature && activeSignature.isVariadic) { + return Math.min(info.argumentIndex, activeSignature.parameters.length - 1) + } + return info.argumentIndex + } + + private convertSignature(item: Proto.SignatureHelpItem): SignatureInformation { + return { + label: Previewer.plain(item.prefixDisplayParts).replace(/\($/, ''), + documentation: Previewer.markdownDocumentation( + item.documentation, + item.tags.filter(x => x.name !== 'param') + ), + parameters: item.parameters.map(p => { + return { + label: Previewer.plain(p.displayParts), + documentation: Previewer.markdownDocumentation(p.documentation, []) + } + }) + } + } +} diff --git a/src/server/features/tagCompletion.ts b/src/server/features/tagCompletion.ts new file mode 100644 index 0000000..15eb50c --- /dev/null +++ b/src/server/features/tagCompletion.ts @@ -0,0 +1,53 @@ +/*--------------------------------------------------------------------------------------------- + * 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, Position, TextDocument } from 'vscode-languageserver-protocol' +import { CompletionItemProvider } from 'coc.nvim/lib/provider' +import * as Proto from '../protocol' +import { ITypeScriptServiceClient } from '../typescriptService' +import * as typeConverters from '../utils/typeConverters' + +export default class TypeScriptTagCompletion implements CompletionItemProvider { + constructor( + private readonly client: ITypeScriptServiceClient + ) { } + + public async provideCompletionItems( + document: TextDocument, + position: Position, + token: CancellationToken, + context: CompletionContext + ): Promise<CompletionItem[] | undefined> { + const filepath = this.client.toPath(document.uri) + if (!filepath) return undefined + if (context.triggerCharacter != '>') { + return undefined + } + + const args: Proto.JsxClosingTagRequestArgs = typeConverters.Position.toFileLocationRequestArgs(filepath, position) + let body: Proto.TextInsertion | undefined + try { + const response = await this.client.execute('jsxClosingTag', args, token) + body = response && response.body + if (!body) { + return undefined + } + } catch { + return undefined + } + + return [this.getCompletion(body)] + } + + private getCompletion(body: Proto.TextInsertion): CompletionItem { + const completion = CompletionItem.create(body.newText) + completion.insertText = this.getTagSnippet(body) // tslint:disable-line + return completion + } + + private getTagSnippet(closingTag: Proto.TextInsertion): string { + let { newText, caretOffset } = closingTag + return newText.slice(0, caretOffset) + '$0' + newText.slice(caretOffset) + } +} diff --git a/src/server/features/updatePathOnRename.ts b/src/server/features/updatePathOnRename.ts new file mode 100644 index 0000000..fff5ff8 --- /dev/null +++ b/src/server/features/updatePathOnRename.ts @@ -0,0 +1,105 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { Disposable, TextDocument, WorkspaceEdit } from 'vscode-languageserver-protocol' +import Uri from 'vscode-uri' +import { disposeAll, workspace } from 'coc.nvim' +import * as Proto from '../protocol' +import { ITypeScriptServiceClient } from '../typescriptService' +import * as typeConverters from '../utils/typeConverters' +import FileConfigurationManager from './fileConfigurationManager' + +function wait(ms: number): Promise<any> { + return new Promise(resolve => { + setTimeout(() => { + resolve() + }, ms) + }) +} + +export default class UpdateImportsOnFileRenameHandler { + private disposables: Disposable[] = [] + + public constructor( + private readonly client: ITypeScriptServiceClient, + private readonly fileConfigurationManager: FileConfigurationManager, + languageId: string + ) { + let glob = languageId == 'typescript' ? '**/*.ts' : '**/*.js' + const watcher = workspace.createFileSystemWatcher(glob) + this.disposables.push(watcher) + watcher.onDidRename(e => { + this.doRename(e.oldUri, e.newUri).catch(e => { + client.logger.error(e.message) + }) + }, null, this.disposables) + } + + public dispose(): void { + disposeAll(this.disposables) + } + + private async doRename( + oldResource: Uri, + newResource: Uri + ): Promise<void> { + if (oldResource.scheme !== 'file' || newResource.scheme !== 'file') { + return + } + const targetFile = newResource.fsPath + const oldFile = oldResource.fsPath + await workspace.openResource(newResource.toString()) + // Make sure TS knows about file + await wait(100) + + let document = workspace.getDocument(newResource.toString()) + if (!document) return + + const edits = await this.getEditsForFileRename( + document.textDocument, + oldFile, + targetFile, + ) + if (!edits) return + + if (await this.promptUser(newResource)) { + await workspace.applyEdit(edits) + } + } + + private async promptUser(newResource: Uri): Promise<boolean> { + const res = await workspace.nvim.call('coc#util#prompt_confirm', [`Update imports for moved file: ${newResource.fsPath} ?`]) + return res == 1 + } + + private async getEditsForFileRename(document: TextDocument, oldFile: string, newFile: string): Promise<WorkspaceEdit> { + await this.fileConfigurationManager.ensureConfigurationForDocument(document) + const args: Proto.GetEditsForFileRenameRequestArgs = { + oldFilePath: oldFile, + newFilePath: newFile + } + const response = await this.client.execute('getEditsForFileRename', args) + if (!response || !response.body) { + return + } + + const edits: Proto.FileCodeEdits[] = [] + for (const edit of response.body) { + // Workaround for https://github.com/Microsoft/vscode/issues/52675 + if ((edit as Proto.FileCodeEdits).fileName.match( + /[\/\\]node_modules[\/\\]/gi + )) { + continue + } + for (const change of (edit as Proto.FileCodeEdits).textChanges) { + if (change.newText.match(/\/node_modules\//gi)) { + continue + } + } + + edits.push(edit) + } + return typeConverters.WorkspaceEdit.fromFileCodeEdits(this.client, edits) + } +} diff --git a/src/server/features/watchBuild.ts b/src/server/features/watchBuild.ts new file mode 100644 index 0000000..e062b37 --- /dev/null +++ b/src/server/features/watchBuild.ts @@ -0,0 +1,172 @@ +import { DiagnosticCollection, disposeAll, Document, languages, workspace } from 'coc.nvim' +import { Command, CommandManager } from 'coc.nvim/lib/commands' +import { resolveRoot } from '../utils/fs' +import fs from 'fs' +import path from 'path' +import { Diagnostic, DiagnosticSeverity, Disposable, Range } from 'vscode-languageserver-protocol' +import Uri from 'vscode-uri' + +const TSC = './node_modules/.bin/tsc' +const countRegex = /Found\s(\d+)\serror/ +const startRegex = /File\s+change\s+detected/ +const errorRegex = /^(.+):(\d+):(\d+)\s-\s(\w+)\s+[A-Za-z]+(\d+):\s+(.*)$/ + +enum TscStatus { + INIT, + COMPILING, + RUNNING, + ERROR, +} + +class WatchCommand implements Command { + public readonly id: string = 'tsserver.watchBuild' + + constructor( + private collection: DiagnosticCollection + ) { + } + + private setStatus(state: TscStatus): void { + let s = 'init' + switch (state) { + case TscStatus.COMPILING: + s = 'compiling' + break + case TscStatus.RUNNING: + s = 'running' + break + case TscStatus.ERROR: + s = 'error' + break + } + workspace.nvim.setVar('tsc_status', s, true) + } + + public async execute(): Promise<void> { + let docs = workspace.documents + let idx = docs.findIndex(doc => doc.uri.indexOf(TSC) !== -1) + if (idx !== -1) return + let document = await workspace.document + let fsPath = Uri.parse(document.uri).fsPath + let cwd = path.dirname(fsPath) + let dir = resolveRoot(cwd, ['node_modules']) + if (dir) { + let file = path.join(dir, 'node_modules/.bin/tsc') + if (!fs.existsSync(file)) dir = null + } + if (!dir) { + workspace.showMessage('typescript module not found!', 'error') + return + } + let configRoot = resolveRoot(cwd, ['tsconfig.json']) + if (!configRoot) { + workspace.showMessage('tsconfig.json not found!', 'error') + return + } + let configPath = path.relative(dir, path.join(configRoot, 'tsconfig.json')) + let cmd = `${TSC} -p ${configPath} --watch true` + await workspace.nvim.call('coc#util#open_terminal', { + keepfocus: 1, + cwd: dir, + cmd + }) + } + + public async onTerminalCreated(doc: Document): Promise<void> { + let entries: Map<string, Diagnostic[]> = new Map() + let cwd = await doc.getcwd() + if (!cwd) return + let uris = new Set() + this.setStatus(TscStatus.RUNNING) + let parseLine = (line: string): void => { + if (startRegex.test(line)) { + this.setStatus(TscStatus.COMPILING) + entries = new Map() + } else if (errorRegex.test(line)) { + let ms = line.match(errorRegex) + let severity = /error/.test(ms[4]) ? DiagnosticSeverity.Error : DiagnosticSeverity.Warning + let lnum = Number(ms[2]) - 1 + let character = Number(ms[3]) - 1 + let range = Range.create(lnum, character, lnum, character) + let uri = Uri.file(path.join(cwd, ms[1])).toString() + let diagnostics = entries.get(uri) || [] + diagnostics.push(Diagnostic.create(range, ms[6], severity, ms[5], 'tsc')) + entries.set(uri, diagnostics) + } else if (countRegex.test(line)) { + let ms = line.match(countRegex) + if (ms[1] == '0') { + entries = new Map() + this.setStatus(TscStatus.RUNNING) + this.collection.clear() + uris = new Set() + return + } + this.setStatus(TscStatus.ERROR) + for (let [key, value] of entries.entries()) { + this.collection.set(key, value) + } + for (let uri of uris) { + if (!entries.has(uri)) { + this.collection.set(uri, []) + } + } + uris = new Set(entries.keys()) + } + } + for (let line of doc.content.split('\n')) { + parseLine(line) + } + doc.onDocumentDetach(() => { + entries = new Map() + this.setStatus(TscStatus.INIT) + this.collection.clear() + }) + doc.onDocumentChange(e => { + let { contentChanges } = e + for (let change of contentChanges) { + let lines = change.text.split('\n') + for (let line of lines) { + parseLine(line) + } + } + }) + } +} + +export default class WatchProject implements Disposable { + private disposables: Disposable[] = [] + public constructor( + commandManager: CommandManager + ) { + let collection = languages.createDiagnosticCollection('tsc') + let cmd = new WatchCommand(collection) + commandManager.register(cmd) + this.disposables.push(Disposable.create(() => { + commandManager.unregister(cmd.id) + })) + workspace.documents.forEach(doc => { + let { uri } = doc + if (this.isTscBuffer(uri)) { + cmd.onTerminalCreated(doc).catch(_e => { + // noop + }) + } + }) + workspace.onDidOpenTextDocument(doc => { + let { uri } = doc + if (this.isTscBuffer(uri)) { + cmd.onTerminalCreated(workspace.getDocument(uri)).catch(_e => { + // noop + }) + } + }, this, this.disposables) + } + + private isTscBuffer(uri: string): boolean { + return uri.startsWith('term://') && uri.indexOf(TSC) !== -1 + } + + public dispose(): void { + disposeAll(this.disposables) + } +} diff --git a/src/server/features/workspaceSymbols.ts b/src/server/features/workspaceSymbols.ts new file mode 100644 index 0000000..0c6c1f3 --- /dev/null +++ b/src/server/features/workspaceSymbols.ts @@ -0,0 +1,96 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { CancellationToken, Range, SymbolInformation, SymbolKind } from 'vscode-languageserver-protocol' +import { WorkspaceSymbolProvider } from 'coc.nvim/lib/provider' +import { workspace } from 'coc.nvim' +import * as Proto from '../protocol' +import { ITypeScriptServiceClient } from '../typescriptService' +import * as typeConverters from '../utils/typeConverters' + +function getSymbolKind(item: Proto.NavtoItem): SymbolKind { + switch (item.kind) { + case 'method': + return SymbolKind.Method + case 'enum': + return SymbolKind.Enum + case 'function': + return SymbolKind.Function + case 'class': + return SymbolKind.Class + case 'interface': + return SymbolKind.Interface + case 'var': + return SymbolKind.Variable + default: + return SymbolKind.Variable + } +} + +export default class TypeScriptWorkspaceSymbolProvider implements WorkspaceSymbolProvider { + public constructor( + private readonly client: ITypeScriptServiceClient, + private readonly languageIds: string[] + ) { } + + public async provideWorkspaceSymbols( + search: string, + token: CancellationToken + ): Promise<SymbolInformation[]> { + const uri = this.getUri() + if (!uri) return [] + + const filepath = this.client.toPath(uri) + if (!filepath) return [] + + const args: Proto.NavtoRequestArgs = { + file: filepath, + searchValue: search + } + + const response = await this.client.execute('navto', args, token) + if (!response.body) return [] + + const result: SymbolInformation[] = [] + for (const item of response.body) { + if (!item.containerName && item.kind === 'alias') { + continue + } + const label = TypeScriptWorkspaceSymbolProvider.getLabel(item) + const range: Range = { + start: typeConverters.Position.fromLocation(item.start), + end: typeConverters.Position.fromLocation(item.end), + } + const symbolInfo = SymbolInformation.create( + label, + getSymbolKind(item), + range, + this.client.toResource(item.file)) + + result.push(symbolInfo) + } + return result + } + + private static getLabel(item: Proto.NavtoItem): string { + let label = item.name + if (item.kind === 'method' || item.kind === 'function') { + label += '()' + } + return label + } + + private getUri(): string { + // typescript wants to have a resource even when asking + // general questions so we check the active editor. If this + // doesn't match we take the first TS document. + const documents = workspace.textDocuments + for (const document of documents) { + if (this.languageIds.indexOf(document.languageId) >= 0) { + return document.uri + } + } + return undefined + } +} diff --git a/src/server/index.ts b/src/server/index.ts new file mode 100644 index 0000000..75c4964 --- /dev/null +++ b/src/server/index.ts @@ -0,0 +1,94 @@ +import { disposeAll, IServiceProvider, ServiceStat, workspace, WorkspaceConfiguration } from 'coc.nvim' +import { Disposable, DocumentSelector, Emitter, Event } from 'vscode-languageserver-protocol' +import URI from 'vscode-uri' +import TypeScriptServiceClientHost from './typescriptServiceClientHost' +import { LanguageDescription, standardLanguageDescriptions } from './utils/languageDescription' + +function wait(ms: number): Promise<any> { + return new Promise(resolve => { + setTimeout(() => { + resolve() + }, ms) + }) +} + +export default class TsserverService implements IServiceProvider { + public id = 'tsserver' + public name = 'tsserver' + public enable: boolean + // supported language types + public selector: DocumentSelector + public state = ServiceStat.Initial + public clientHost: TypeScriptServiceClientHost + private _onDidServiceReady = new Emitter<void>() + public readonly onServiceReady: Event<void> = this._onDidServiceReady.event + private readonly disposables: Disposable[] = [] + private descriptions: LanguageDescription[] = [] + + constructor() { + const config = workspace.getConfiguration('tsserver') + const enableJavascript = !!config.get<boolean>('enableJavascript') + this.enable = config.get<boolean>('enable') + this.descriptions = standardLanguageDescriptions.filter(o => { + return enableJavascript ? true : o.id != 'javascript' + }) + this.selector = this.descriptions.reduce((arr, c) => { + return arr.concat(c.modeIds) + }, []) + } + + public get config(): WorkspaceConfiguration { + return workspace.getConfiguration('tsserver') + } + + public start(): Promise<void> { + this.clientHost = new TypeScriptServiceClientHost(this.descriptions) + this.disposables.push(this.clientHost) + Object.defineProperty(this, 'state', { + get: () => { + return this.clientHost.serviceClient.state + } + }) + let client = this.clientHost.serviceClient + return new Promise(resolve => { + let started = false + client.onTsServerStarted(() => { + this._onDidServiceReady.fire(void 0) + this.ensureConfiguration() // tslint:disable-line + if (!started) { + started = true + resolve() + } + }) + }) + } + + private async ensureConfiguration(): Promise<void> { + if (!this.clientHost) return + let document = await workspace.document + await wait(100) + + let uri = URI.parse(document.uri) + let language = this.clientHost.findLanguage(uri) + if (!language) return + await language.fileConfigurationManager.ensureConfigurationForDocument(document.textDocument) + } + + public dispose(): void { + disposeAll(this.disposables) + } + + public async restart(): Promise<void> { + if (!this.clientHost) return + let client = this.clientHost.serviceClient + await client.restartTsServer() + } + + public async stop(): Promise<void> { + if (!this.clientHost) return + this.clientHost.reset() + let client = this.clientHost.serviceClient + await client.stop() + return + } +} diff --git a/src/server/languageProvider.ts b/src/server/languageProvider.ts new file mode 100644 index 0000000..d5a81b1 --- /dev/null +++ b/src/server/languageProvider.ts @@ -0,0 +1,349 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { Diagnostic, Disposable } from 'vscode-languageserver-protocol' +import Uri from 'vscode-uri' +import { workspace, commands, events, languages, DiagnosticKind, ServiceStat, disposeAll } from 'coc.nvim' +import { CachedNavTreeResponse } from './features/baseCodeLensProvider' +import BufferSyncSupport from './features/bufferSyncSupport' +import CompletionItemProvider from './features/completionItemProvider' +import DefinitionProvider from './features/definitionProvider' +import { DiagnosticsManager } from './features/diagnostics' +import DirectiveCommentCompletionProvider from './features/directiveCommentCompletions' +import DocumentHighlight from './features/documentHighlight' +import DocumentSymbolProvider from './features/documentSymbol' +import FileConfigurationManager from './features/fileConfigurationManager' +import Folding from './features/folding' +import FormattingProvider from './features/formatting' +import HoverProvider from './features/hover' +import ImplementationsCodeLensProvider from './features/implementationsCodeLens' +import OrganizeImportsProvider from './features/organizeImports' +// import TagCompletionProvider from './features/tagCompletion' +import QuickfixProvider from './features/quickfix' +import RefactorProvider from './features/refactor' +import ReferenceProvider from './features/references' +import ReferencesCodeLensProvider from './features/referencesCodeLens' +import RenameProvider from './features/rename' +import SignatureHelpProvider from './features/signatureHelp' +import UpdateImportsOnFileRenameHandler from './features/updatePathOnRename' +import WatchBuild from './features/watchBuild' +import WorkspaceSymbolProvider from './features/workspaceSymbols' +import TypeScriptServiceClient from './typescriptServiceClient' +import API from './utils/api' +import { LanguageDescription } from './utils/languageDescription' +import TypingsStatus from './utils/typingsStatus' + +const validateSetting = 'validate.enable' +const suggestionSetting = 'suggestionActions.enabled' + +export default class LanguageProvider { + private readonly diagnosticsManager: DiagnosticsManager + private readonly bufferSyncSupport: BufferSyncSupport + public readonly fileConfigurationManager: FileConfigurationManager // tslint:disable-line + private _validate = true + private _enableSuggestionDiagnostics = true + private readonly disposables: Disposable[] = [] + + constructor( + public client: TypeScriptServiceClient, + private description: LanguageDescription, + typingsStatus: TypingsStatus + ) { + this.fileConfigurationManager = new FileConfigurationManager(client) + this.bufferSyncSupport = new BufferSyncSupport( + client, + description.modeIds, + this._validate + ) + this.diagnosticsManager = new DiagnosticsManager() + this.disposables.push(this.diagnosticsManager) + + client.onTsServerStarted(async () => { + let document = await workspace.document + if (description.modeIds.indexOf(document.filetype) !== -1) { + this.fileConfigurationManager.ensureConfigurationForDocument(document.textDocument) // tslint:disable-line + } + }) + + events.on('BufEnter', bufnr => { + let doc = workspace.getDocument(bufnr) + if (!doc) return + if (description.modeIds.indexOf(doc.filetype) == -1) return + if (client.state !== ServiceStat.Running) return + this.fileConfigurationManager.ensureConfigurationForDocument(doc.textDocument) // tslint:disable-line + }, this, this.disposables) + + workspace.onDidChangeConfiguration(this.configurationChanged, this, this.disposables) + + let initialized = false + + client.onTsServerStarted(() => { // tslint:disable-line + if (!initialized) { + initialized = true + this.registerProviders(client, typingsStatus) + this.bufferSyncSupport.listen() + } else { + this.reInitialize() + } + }) + } + + public dispose(): void { + disposeAll(this.disposables) + this.bufferSyncSupport.dispose() + } + + private configurationChanged(): void { + const config = workspace.getConfiguration(this.id) + this.updateValidate(config.get(validateSetting, true)) + this.updateSuggestionDiagnostics(config.get(suggestionSetting, true)) + } + + private registerProviders( + client: TypeScriptServiceClient, + typingsStatus: TypingsStatus + ): void { + let languageIds = this.description.modeIds + + this.disposables.push( + languages.registerCompletionItemProvider( + `tsserver-${this.description.id}`, + 'TSC', + languageIds, + new CompletionItemProvider( + client, + typingsStatus, + this.fileConfigurationManager, + this.description.id + ), + CompletionItemProvider.triggerCharacters + ) + ) + + if (this.client.apiVersion.gte(API.v230)) { + this.disposables.push( + languages.registerCompletionItemProvider( + `${this.description.id}-directive`, + 'TSC', + languageIds, + new DirectiveCommentCompletionProvider( + client, + ), + ['@'] + ) + ) + } + let definitionProvider = new DefinitionProvider(client) + + this.disposables.push( + languages.registerDefinitionProvider( + languageIds, + definitionProvider + ) + ) + + this.disposables.push( + languages.registerTypeDefinitionProvider( + languageIds, + definitionProvider + ) + ) + + this.disposables.push( + languages.registerImplementationProvider( + languageIds, + definitionProvider + ) + ) + + this.disposables.push( + languages.registerReferencesProvider( + languageIds, + new ReferenceProvider(client) + ) + ) + + this.disposables.push( + languages.registerHoverProvider( + languageIds, + new HoverProvider(client)) + ) + + this.disposables.push( + languages.registerDocumentHighlightProvider(languageIds, new DocumentHighlight(this.client)) + ) + + this.disposables.push( + languages.registerSignatureHelpProvider( + languageIds, + new SignatureHelpProvider(client)) + ) + + this.disposables.push( + languages.registerDocumentSymbolProvider( + languageIds, + new DocumentSymbolProvider(client)) + ) + + this.disposables.push( + languages.registerWorkspaceSymbolProvider( + languageIds, + new WorkspaceSymbolProvider(client, languageIds)) + ) + + this.disposables.push( + languages.registerRenameProvider( + languageIds, + new RenameProvider(client)) + ) + let formatProvider = new FormattingProvider(client, this.fileConfigurationManager) + this.disposables.push( + languages.registerDocumentFormatProvider(languageIds, formatProvider) + ) + this.disposables.push( + languages.registerDocumentRangeFormatProvider(languageIds, formatProvider) + ) + this.disposables.push( + languages.registerOnTypeFormattingEditProvider(languageIds, formatProvider, [';', '}', '\n']) + ) + + // this.disposables.push( + // new ProjectError(client, commandManager) + // ) + + if (this.client.apiVersion.gte(API.v280)) { + this.disposables.push( + new OrganizeImportsProvider(client, commands, this.fileConfigurationManager, this.description.id) + ) + + this.disposables.push( + languages.registerFoldingRangeProvider(languageIds, new Folding(this.client)) + ) + } + + let { fileConfigurationManager } = this + let conf = fileConfigurationManager.getLanguageConfiguration(this.id) + + if (this.client.apiVersion.gte(API.v290) + && conf.get<boolean>('updateImportsOnFileMove.enable')) { + this.disposables.push( + new UpdateImportsOnFileRenameHandler(client, this.fileConfigurationManager, this.id) + ) + } + + if (this.client.apiVersion.gte(API.v240)) { + this.disposables.push( + languages.registerCodeActionProvider( + languageIds, + new RefactorProvider(client, this.fileConfigurationManager))) + } + + this.disposables.push( + languages.registerCodeActionProvider( + languageIds, + new QuickfixProvider(client, this.diagnosticsManager, this.bufferSyncSupport))) + let cachedResponse = new CachedNavTreeResponse() + if (this.client.apiVersion.gte(API.v206) + && conf.get<boolean>('referencesCodeLens.enable')) { + this.disposables.push( + languages.registerCodeLensProvider( + languageIds, + new ReferencesCodeLensProvider(client, cachedResponse))) + } + + if (this.client.apiVersion.gte(API.v220) + && conf.get<boolean>('implementationsCodeLens.enable')) { + this.disposables.push( + languages.registerCodeLensProvider( + languageIds, + new ImplementationsCodeLensProvider(client, cachedResponse))) + } + + if (this.description.id == 'typescript') { + this.disposables.push( + new WatchBuild(commands) + ) + } + + // if (this.client.apiVersion.gte(API.v300)) { + // this.disposables.push( + // languages.registerCompletionItemProvider( + // `tsserver-${this.description.id}-tag`, + // 'TSC', + // languageIds, + // new TagCompletionProvider(client), + // ['>'] + // ) + // ) + // } + } + + public handles(resource: Uri): boolean { + let doc = workspace.getDocument(resource.toString()) + let { modeIds } = this.description + if (doc && modeIds.indexOf(doc.filetype) !== -1) { + return true + } + let str = resource.toString() + if (this.id === 'typescript' && /\.ts(x)?$/.test(str)) { + return true + } + if (this.id === 'javascript' && /\.js(x)?$/.test(str)) { + return true + } + return false + } + + private get id(): string { // tslint:disable-line + return this.description.id + } + + public get diagnosticSource(): string { + return this.description.diagnosticSource + } + + private updateValidate(value: boolean): void { + if (this._validate === value) { + return + } + this._validate = value + this.bufferSyncSupport.validate = value + this.diagnosticsManager.validate = value + if (value) { + this.triggerAllDiagnostics() + } + } + + private updateSuggestionDiagnostics(value: boolean): void { + if (this._enableSuggestionDiagnostics === value) { + return + } + this._enableSuggestionDiagnostics = value + this.diagnosticsManager.enableSuggestions = value + if (value) { + this.triggerAllDiagnostics() + } + } + + public reInitialize(): void { + this.diagnosticsManager.reInitialize() + this.bufferSyncSupport.reInitialize() + } + + public triggerAllDiagnostics(): void { + this.bufferSyncSupport.requestAllDiagnostics() + } + + public diagnosticsReceived( + diagnosticsKind: DiagnosticKind, + file: Uri, + diagnostics: Diagnostic[] + ): void { + this.diagnosticsManager.diagnosticsReceived( + diagnosticsKind, + file.toString(), + diagnostics + ) + } +} diff --git a/src/server/protocol.const.ts b/src/server/protocol.const.ts new file mode 100644 index 0000000..54010a6 --- /dev/null +++ b/src/server/protocol.const.ts @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +export class Kind { + public static readonly alias = 'alias' + public static readonly callSignature = 'call' + public static readonly class = 'class' + public static readonly const = 'const' + public static readonly constructorImplementation = 'constructor' + public static readonly constructSignature = 'construct' + public static readonly directory = 'directory' + public static readonly enum = 'enum' + public static readonly externalModuleName = 'external module name' + public static readonly file = 'file' + public static readonly function = 'function' + public static readonly indexSignature = 'index' + public static readonly interface = 'interface' + public static readonly keyword = 'keyword' + 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 memberGetAccessor = 'getter' + public static readonly memberSetAccessor = 'setter' + public static readonly memberVariable = 'property' + public static readonly module = 'module' + public static readonly primitiveType = 'primitive type' + public static readonly script = 'script' + public static readonly type = 'type' + public static readonly variable = 'var' + public static readonly warning = 'warning' +} + +export class DiagnosticCategory { + public static readonly error = 'error' + public static readonly warning = 'warning' + public static readonly suggestion = 'suggestion' +} diff --git a/src/server/protocol.d.ts b/src/server/protocol.d.ts new file mode 100644 index 0000000..dfaa3d9 --- /dev/null +++ b/src/server/protocol.d.ts @@ -0,0 +1,2475 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +declare namespace ts.server.protocol { + const enum CommandTypes { + JsxClosingTag = "jsxClosingTag", + Brace = "brace", + BraceCompletion = "braceCompletion", + GetSpanOfEnclosingComment = "getSpanOfEnclosingComment", + Change = "change", + Close = "close", + /** @deprecated Prefer CompletionInfo -- see comment on CompletionsResponse */ + Completions = "completions", + CompletionInfo = "completionInfo", + CompletionDetails = "completionEntryDetails", + CompileOnSaveAffectedFileList = "compileOnSaveAffectedFileList", + CompileOnSaveEmitFile = "compileOnSaveEmitFile", + Configure = "configure", + Definition = "definition", + DefinitionAndBoundSpan = "definitionAndBoundSpan", + Implementation = "implementation", + Exit = "exit", + Format = "format", + Formatonkey = "formatonkey", + Geterr = "geterr", + GeterrForProject = "geterrForProject", + SemanticDiagnosticsSync = "semanticDiagnosticsSync", + SyntacticDiagnosticsSync = "syntacticDiagnosticsSync", + SuggestionDiagnosticsSync = "suggestionDiagnosticsSync", + NavBar = "navbar", + Navto = "navto", + NavTree = "navtree", + NavTreeFull = "navtree-full", + /** @deprecated */ + Occurrences = "occurrences", + DocumentHighlights = "documentHighlights", + Open = "open", + Quickinfo = "quickinfo", + References = "references", + Reload = "reload", + Rename = "rename", + Saveto = "saveto", + SignatureHelp = "signatureHelp", + Status = "status", + TypeDefinition = "typeDefinition", + ProjectInfo = "projectInfo", + ReloadProjects = "reloadProjects", + Unknown = "unknown", + OpenExternalProject = "openExternalProject", + OpenExternalProjects = "openExternalProjects", + CloseExternalProject = "closeExternalProject", + GetOutliningSpans = "getOutliningSpans", + TodoComments = "todoComments", + Indentation = "indentation", + DocCommentTemplate = "docCommentTemplate", + CompilerOptionsForInferredProjects = "compilerOptionsForInferredProjects", + GetCodeFixes = "getCodeFixes", + GetCombinedCodeFix = "getCombinedCodeFix", + ApplyCodeActionCommand = "applyCodeActionCommand", + GetSupportedCodeFixes = "getSupportedCodeFixes", + GetApplicableRefactors = "getApplicableRefactors", + GetEditsForRefactor = "getEditsForRefactor", + OrganizeImports = "organizeImports", + GetEditsForFileRename = "getEditsForFileRename" + } + /** + * A TypeScript Server message + */ + interface Message { + /** + * Sequence number of the message + */ + seq: number; + /** + * One of "request", "response", or "event" + */ + type: "request" | "response" | "event"; + } + /** + * Client-initiated request message + */ + interface Request extends Message { + type: "request"; + /** + * The command to execute + */ + command: string; + /** + * Object containing arguments for the command + */ + arguments?: any; + } + /** + * Request to reload the project structure for all the opened files + */ + interface ReloadProjectsRequest extends Message { + command: CommandTypes.ReloadProjects; + } + /** + * Server-initiated event message + */ + interface Event extends Message { + type: "event"; + /** + * Name of event + */ + event: string; + /** + * Event-specific information + */ + body?: any; + } + /** + * Response by server to client request message. + */ + interface Response extends Message { + type: "response"; + /** + * Sequence number of the request message. + */ + request_seq: number; + /** + * Outcome of the request. + */ + success: boolean; + /** + * The command requested. + */ + command: string; + /** + * If success === false, this should always be provided. + * Otherwise, may (or may not) contain a success message. + */ + message?: string; + /** + * Contains message body if success === true. + */ + body?: any; + } + /** + * Arguments for FileRequest messages. + */ + interface FileRequestArgs { + /** + * The file for the request (absolute pathname required). + */ + file: string; + projectFileName?: string; + } + interface StatusRequest extends Request { + command: CommandTypes.Status; + } + interface StatusResponseBody { + /** + * The TypeScript version (`ts.version`). + */ + version: string; + } + /** + * Response to StatusRequest + */ + interface StatusResponse extends Response { + body: StatusResponseBody; + } + /** + * Requests a JS Doc comment template for a given position + */ + interface DocCommentTemplateRequest extends FileLocationRequest { + command: CommandTypes.DocCommentTemplate; + } + /** + * Response to DocCommentTemplateRequest + */ + interface DocCommandTemplateResponse extends Response { + body?: TextInsertion; + } + /** + * A request to get TODO comments from the file + */ + interface TodoCommentRequest extends FileRequest { + command: CommandTypes.TodoComments; + arguments: TodoCommentRequestArgs; + } + /** + * Arguments for TodoCommentRequest request. + */ + interface TodoCommentRequestArgs extends FileRequestArgs { + /** + * Array of target TodoCommentDescriptors that describes TODO comments to be found + */ + descriptors: TodoCommentDescriptor[]; + } + /** + * Response for TodoCommentRequest request. + */ + interface TodoCommentsResponse extends Response { + body?: TodoComment[]; + } + /** + * A request to determine if the caret is inside a comment. + */ + interface SpanOfEnclosingCommentRequest extends FileLocationRequest { + command: CommandTypes.GetSpanOfEnclosingComment; + arguments: SpanOfEnclosingCommentRequestArgs; + } + interface SpanOfEnclosingCommentRequestArgs extends FileLocationRequestArgs { + /** + * Requires that the enclosing span be a multi-line comment, or else the request returns undefined. + */ + onlyMultiLine: boolean; + } + /** + * Request to obtain outlining spans in file. + */ + interface OutliningSpansRequest extends FileRequest { + command: CommandTypes.GetOutliningSpans; + } + interface OutliningSpan { + /** The span of the document to actually collapse. */ + textSpan: TextSpan; + /** The span of the document to display when the user hovers over the collapsed span. */ + hintSpan: TextSpan; + /** The text to display in the editor for the collapsed region. */ + bannerText: string; + /** + * Whether or not this region should be automatically collapsed when + * the 'Collapse to Definitions' command is invoked. + */ + autoCollapse: boolean; + /** + * Classification of the contents of the span + */ + kind: OutliningSpanKind; + } + /** + * Response to OutliningSpansRequest request. + */ + interface OutliningSpansResponse extends Response { + body?: OutliningSpan[]; + } + /** + * A request to get indentation for a location in file + */ + interface IndentationRequest extends FileLocationRequest { + command: CommandTypes.Indentation; + arguments: IndentationRequestArgs; + } + /** + * Response for IndentationRequest request. + */ + interface IndentationResponse extends Response { + body?: IndentationResult; + } + /** + * Indentation result representing where indentation should be placed + */ + interface IndentationResult { + /** + * The base position in the document that the indent should be relative to + */ + position: number; + /** + * The number of columns the indent should be at relative to the position's column. + */ + indentation: number; + } + /** + * Arguments for IndentationRequest request. + */ + interface IndentationRequestArgs extends FileLocationRequestArgs { + /** + * An optional set of settings to be used when computing indentation. + * If argument is omitted - then it will use settings for file that were previously set via 'configure' request or global settings. + */ + options?: EditorSettings; + } + /** + * Arguments for ProjectInfoRequest request. + */ + interface ProjectInfoRequestArgs extends FileRequestArgs { + /** + * Indicate if the file name list of the project is needed + */ + needFileNameList: boolean; + } + /** + * A request to get the project information of the current file. + */ + interface ProjectInfoRequest extends Request { + command: CommandTypes.ProjectInfo; + arguments: ProjectInfoRequestArgs; + } + /** + * A request to retrieve compiler options diagnostics for a project + */ + interface CompilerOptionsDiagnosticsRequest extends Request { + arguments: CompilerOptionsDiagnosticsRequestArgs; + } + /** + * Arguments for CompilerOptionsDiagnosticsRequest request. + */ + interface CompilerOptionsDiagnosticsRequestArgs { + /** + * Name of the project to retrieve compiler options diagnostics. + */ + projectFileName: string; + } + /** + * Response message body for "projectInfo" request + */ + interface ProjectInfo { + /** + * For configured project, this is the normalized path of the 'tsconfig.json' file + * For inferred project, this is undefined + */ + configFileName: string; + /** + * The list of normalized file name in the project, including 'lib.d.ts' + */ + fileNames?: string[]; + /** + * Indicates if the project has a active language service instance + */ + languageServiceDisabled?: boolean; + } + /** + * Represents diagnostic info that includes location of diagnostic in two forms + * - start position and length of the error span + * - startLocation and endLocation - a pair of Location objects that store start/end line and offset of the error span. + */ + interface DiagnosticWithLinePosition { + message: string; + start: number; + length: number; + startLocation: Location; + endLocation: Location; + category: string; + code: number; + /** May store more in future. For now, this will simply be `true` to indicate when a diagnostic is an unused-identifier diagnostic. */ + reportsUnnecessary?: {}; + relatedInformation?: DiagnosticRelatedInformation[]; + } + /** + * Response message for "projectInfo" request + */ + interface ProjectInfoResponse extends Response { + body?: ProjectInfo; + } + /** + * Request whose sole parameter is a file name. + */ + interface FileRequest extends Request { + arguments: FileRequestArgs; + } + /** + * Instances of this interface specify a location in a source file: + * (file, line, character offset), where line and character offset are 1-based. + */ + interface FileLocationRequestArgs extends FileRequestArgs { + /** + * The line number for the request (1-based). + */ + line: number; + /** + * The character offset (on the line) for the request (1-based). + */ + offset: number; + } + type FileLocationOrRangeRequestArgs = FileLocationRequestArgs | FileRangeRequestArgs; + /** + * Request refactorings at a given position or selection area. + */ + interface GetApplicableRefactorsRequest extends Request { + command: CommandTypes.GetApplicableRefactors; + arguments: GetApplicableRefactorsRequestArgs; + } + type GetApplicableRefactorsRequestArgs = FileLocationOrRangeRequestArgs; + /** + * Response is a list of available refactorings. + * Each refactoring exposes one or more "Actions"; a user selects one action to invoke a refactoring + */ + interface GetApplicableRefactorsResponse extends Response { + body?: ApplicableRefactorInfo[]; + } + /** + * A set of one or more available refactoring actions, grouped under a parent refactoring. + */ + interface ApplicableRefactorInfo { + /** + * The programmatic name of the refactoring + */ + name: string; + /** + * A description of this refactoring category to show to the user. + * If the refactoring gets inlined (see below), this text will not be visible. + */ + description: string; + /** + * Inlineable refactorings can have their actions hoisted out to the top level + * of a context menu. Non-inlineanable refactorings should always be shown inside + * their parent grouping. + * + * If not specified, this value is assumed to be 'true' + */ + inlineable?: boolean; + actions: RefactorActionInfo[]; + } + /** + * Represents a single refactoring action - for example, the "Extract Method..." refactor might + * offer several actions, each corresponding to a surround class or closure to extract into. + */ + interface RefactorActionInfo { + /** + * The programmatic name of the refactoring action + */ + name: string; + /** + * A description of this refactoring action to show to the user. + * If the parent refactoring is inlined away, this will be the only text shown, + * so this description should make sense by itself if the parent is inlineable=true + */ + description: string; + } + interface GetEditsForRefactorRequest extends Request { + command: CommandTypes.GetEditsForRefactor; + arguments: GetEditsForRefactorRequestArgs; + } + /** + * Request the edits that a particular refactoring action produces. + * Callers must specify the name of the refactor and the name of the action. + */ + type GetEditsForRefactorRequestArgs = FileLocationOrRangeRequestArgs & { + refactor: string; + action: string; + }; + interface GetEditsForRefactorResponse extends Response { + body?: RefactorEditInfo; + } + interface RefactorEditInfo { + edits: FileCodeEdits[]; + /** + * An optional location where the editor should start a rename operation once + * the refactoring edits have been applied + */ + renameLocation?: Location; + renameFilename?: string; + } + /** + * Organize imports by: + * 1) Removing unused imports + * 2) Coalescing imports from the same module + * 3) Sorting imports + */ + interface OrganizeImportsRequest extends Request { + command: CommandTypes.OrganizeImports; + arguments: OrganizeImportsRequestArgs; + } + type OrganizeImportsScope = GetCombinedCodeFixScope; + interface OrganizeImportsRequestArgs { + scope: OrganizeImportsScope; + } + interface OrganizeImportsResponse extends Response { + body: ReadonlyArray<FileCodeEdits>; + } + interface GetEditsForFileRenameRequest extends Request { + command: CommandTypes.GetEditsForFileRename; + arguments: GetEditsForFileRenameRequestArgs; + } + /** Note: Paths may also be directories. */ + interface GetEditsForFileRenameRequestArgs { + readonly oldFilePath: string; + readonly newFilePath: string; + } + interface GetEditsForFileRenameResponse extends Response { + body: ReadonlyArray<FileCodeEdits>; + } + /** + * Request for the available codefixes at a specific position. + */ + interface CodeFixRequest extends Request { + command: CommandTypes.GetCodeFixes; + arguments: CodeFixRequestArgs; + } + interface GetCombinedCodeFixRequest extends Request { + command: CommandTypes.GetCombinedCodeFix; + arguments: GetCombinedCodeFixRequestArgs; + } + interface GetCombinedCodeFixResponse extends Response { + body: CombinedCodeActions; + } + interface ApplyCodeActionCommandRequest extends Request { + command: CommandTypes.ApplyCodeActionCommand; + arguments: ApplyCodeActionCommandRequestArgs; + } + interface ApplyCodeActionCommandResponse extends Response { + } + interface FileRangeRequestArgs extends FileRequestArgs { + /** + * The line number for the request (1-based). + */ + startLine: number; + /** + * The character offset (on the line) for the request (1-based). + */ + startOffset: number; + /** + * The line number for the request (1-based). + */ + endLine: number; + /** + * The character offset (on the line) for the request (1-based). + */ + endOffset: number; + } + /** + * Instances of this interface specify errorcodes on a specific location in a sourcefile. + */ + interface CodeFixRequestArgs extends FileRangeRequestArgs { + /** + * Errorcodes we want to get the fixes for. + */ + errorCodes?: ReadonlyArray<number>; + } + interface GetCombinedCodeFixRequestArgs { + scope: GetCombinedCodeFixScope; + fixId: {}; + } + interface GetCombinedCodeFixScope { + type: "file"; + args: FileRequestArgs; + } + interface ApplyCodeActionCommandRequestArgs { + /** May also be an array of commands. */ + command: {}; + } + /** + * Response for GetCodeFixes request. + */ + interface GetCodeFixesResponse extends Response { + body?: CodeAction[]; + } + /** + * A request whose arguments specify a file location (file, line, col). + */ + interface FileLocationRequest extends FileRequest { + arguments: FileLocationRequestArgs; + } + /** + * A request to get codes of supported code fixes. + */ + interface GetSupportedCodeFixesRequest extends Request { + command: CommandTypes.GetSupportedCodeFixes; + } + /** + * A response for GetSupportedCodeFixesRequest request. + */ + interface GetSupportedCodeFixesResponse extends Response { + /** + * List of error codes supported by the server. + */ + body?: string[]; + } + /** + * Arguments for EncodedSemanticClassificationsRequest request. + */ + interface EncodedSemanticClassificationsRequestArgs extends FileRequestArgs { + /** + * Start position of the span. + */ + start: number; + /** + * Length of the span. + */ + length: number; + } + /** + * Arguments in document highlight request; include: filesToSearch, file, + * line, offset. + */ + interface DocumentHighlightsRequestArgs extends FileLocationRequestArgs { + /** + * List of files to search for document highlights. + */ + filesToSearch: string[]; + } + /** + * Go to definition request; value of command field is + * "definition". Return response giving the file locations that + * define the symbol found in file at location line, col. + */ + interface DefinitionRequest extends FileLocationRequest { + command: CommandTypes.Definition; + } + interface DefinitionAndBoundSpanRequest extends FileLocationRequest { + readonly command: CommandTypes.DefinitionAndBoundSpan; + } + interface DefinitionAndBoundSpanResponse extends Response { + readonly body: DefinitionInfoAndBoundSpan; + } + /** + * Go to type request; value of command field is + * "typeDefinition". Return response giving the file locations that + * define the type for the symbol found in file at location line, col. + */ + interface TypeDefinitionRequest extends FileLocationRequest { + command: CommandTypes.TypeDefinition; + } + /** + * Go to implementation request; value of command field is + * "implementation". Return response giving the file locations that + * implement the symbol found in file at location line, col. + */ + interface ImplementationRequest extends FileLocationRequest { + command: CommandTypes.Implementation; + } + /** + * Location in source code expressed as (one-based) line and (one-based) column offset. + */ + interface Location { + line: number; + offset: number; + } + /** + * Object found in response messages defining a span of text in source code. + */ + interface TextSpan { + /** + * First character of the definition. + */ + start: Location; + /** + * One character past last character of the definition. + */ + end: Location; + } + /** + * Object found in response messages defining a span of text in a specific source file. + */ + interface FileSpan extends TextSpan { + /** + * File containing text span. + */ + file: string; + } + interface DefinitionInfoAndBoundSpan { + definitions: ReadonlyArray<FileSpan>; + textSpan: TextSpan; + } + /** + * Definition response message. Gives text range for definition. + */ + interface DefinitionResponse extends Response { + body?: FileSpan[]; + } + interface DefinitionInfoAndBoundSpanReponse extends Response { + body?: DefinitionInfoAndBoundSpan; + } + /** + * Definition response message. Gives text range for definition. + */ + interface TypeDefinitionResponse extends Response { + body?: FileSpan[]; + } + /** + * Implementation response message. Gives text range for implementations. + */ + interface ImplementationResponse extends Response { + body?: FileSpan[]; + } + /** + * Request to get brace completion for a location in the file. + */ + interface BraceCompletionRequest extends FileLocationRequest { + command: CommandTypes.BraceCompletion; + arguments: BraceCompletionRequestArgs; + } + /** + * Argument for BraceCompletionRequest request. + */ + interface BraceCompletionRequestArgs extends FileLocationRequestArgs { + /** + * Kind of opening brace + */ + openingBrace: string; + } + interface JsxClosingTagRequest extends FileLocationRequest { + readonly command: CommandTypes.JsxClosingTag; + readonly arguments: JsxClosingTagRequestArgs; + } + interface JsxClosingTagRequestArgs extends FileLocationRequestArgs { + } + interface JsxClosingTagResponse extends Response { + readonly body: TextInsertion; + } + /** + * @deprecated + * Get occurrences request; value of command field is + * "occurrences". Return response giving spans that are relevant + * in the file at a given line and column. + */ + interface OccurrencesRequest extends FileLocationRequest { + command: CommandTypes.Occurrences; + } + /** @deprecated */ + interface OccurrencesResponseItem extends FileSpan { + /** + * True if the occurrence is a write location, false otherwise. + */ + isWriteAccess: boolean; + /** + * True if the occurrence is in a string, undefined otherwise; + */ + isInString?: true; + } + /** @deprecated */ + interface OccurrencesResponse extends Response { + body?: OccurrencesResponseItem[]; + } + /** + * Get document highlights request; value of command field is + * "documentHighlights". Return response giving spans that are relevant + * in the file at a given line and column. + */ + interface DocumentHighlightsRequest extends FileLocationRequest { + command: CommandTypes.DocumentHighlights; + arguments: DocumentHighlightsRequestArgs; + } + /** + * Span augmented with extra information that denotes the kind of the highlighting to be used for span. + */ + interface HighlightSpan extends TextSpan { + kind: HighlightSpanKind; + } + /** + * Represents a set of highligh spans for a give name + */ + interface DocumentHighlightsItem { + /** + * File containing highlight spans. + */ + file: string; + /** + * Spans to highlight in file. + */ + highlightSpans: HighlightSpan[]; + } + /** + * Response for a DocumentHighlightsRequest request. + */ + interface DocumentHighlightsResponse extends Response { + body?: DocumentHighlightsItem[]; + } + /** + * Find references request; value of command field is + * "references". Return response giving the file locations that + * reference the symbol found in file at location line, col. + */ + interface ReferencesRequest extends FileLocationRequest { + command: CommandTypes.References; + } + interface ReferencesResponseItem extends FileSpan { + /** Text of line containing the reference. Including this + * with the response avoids latency of editor loading files + * to show text of reference line (the server already has + * loaded the referencing files). + */ + lineText: string; + /** + * True if reference is a write location, false otherwise. + */ + isWriteAccess: boolean; + /** + * True if reference is a definition, false otherwise. + */ + isDefinition: boolean; + } + /** + * The body of a "references" response message. + */ + interface ReferencesResponseBody { + /** + * The file locations referencing the symbol. + */ + refs: ReferencesResponseItem[]; + /** + * The name of the symbol. + */ + symbolName: string; + /** + * The start character offset of the symbol (on the line provided by the references request). + */ + symbolStartOffset: number; + /** + * The full display name of the symbol. + */ + symbolDisplayString: string; + } + /** + * Response to "references" request. + */ + interface ReferencesResponse extends Response { + body?: ReferencesResponseBody; + } + /** + * Argument for RenameRequest request. + */ + interface RenameRequestArgs extends FileLocationRequestArgs { + /** + * Should text at specified location be found/changed in comments? + */ + findInComments?: boolean; + /** + * Should text at specified location be found/changed in strings? + */ + findInStrings?: boolean; + } + /** + * Rename request; value of command field is "rename". Return + * response giving the file locations that reference the symbol + * found in file at location line, col. Also return full display + * name of the symbol so that client can print it unambiguously. + */ + interface RenameRequest extends FileLocationRequest { + command: CommandTypes.Rename; + arguments: RenameRequestArgs; + } + /** + * Information about the item to be renamed. + */ + interface RenameInfo { + /** + * True if item can be renamed. + */ + canRename: boolean; + /** + * Error message if item can not be renamed. + */ + localizedErrorMessage?: string; + /** + * Display name of the item to be renamed. + */ + displayName: string; + /** + * Full display name of item to be renamed. + */ + fullDisplayName: string; + /** + * The items's kind (such as 'className' or 'parameterName' or plain 'text'). + */ + kind: ScriptElementKind; + /** + * Optional modifiers for the kind (such as 'public'). + */ + kindModifiers: string; + } + /** + * A group of text spans, all in 'file'. + */ + interface SpanGroup { + /** The file to which the spans apply */ + file: string; + /** The text spans in this group */ + locs: TextSpan[]; + } + interface RenameResponseBody { + /** + * Information about the item to be renamed. + */ + info: RenameInfo; + /** + * An array of span groups (one per file) that refer to the item to be renamed. + */ + locs: ReadonlyArray<SpanGroup>; + } + /** + * Rename response message. + */ + interface RenameResponse extends Response { + body?: RenameResponseBody; + } + /** + * Represents a file in external project. + * External project is project whose set of files, compilation options and open\close state + * is maintained by the client (i.e. if all this data come from .csproj file in Visual Studio). + * External project will exist even if all files in it are closed and should be closed explicitly. + * If external project includes one or more tsconfig.json/jsconfig.json files then tsserver will + * create configured project for every config file but will maintain a link that these projects were created + * as a result of opening external project so they should be removed once external project is closed. + */ + interface ExternalFile { + /** + * Name of file file + */ + fileName: string; + /** + * Script kind of the file + */ + scriptKind?: ScriptKindName | ts.ScriptKind; + /** + * Whether file has mixed content (i.e. .cshtml file that combines html markup with C#/JavaScript) + */ + hasMixedContent?: boolean; + /** + * Content of the file + */ + content?: string; + } + /** + * Represent an external project + */ + interface ExternalProject { + /** + * Project name + */ + projectFileName: string; + /** + * List of root files in project + */ + rootFiles: ExternalFile[]; + /** + * Compiler options for the project + */ + options: ExternalProjectCompilerOptions; + /** + * @deprecated typingOptions. Use typeAcquisition instead + */ + typingOptions?: TypeAcquisition; + /** + * Explicitly specified type acquisition for the project + */ + typeAcquisition?: TypeAcquisition; + } + interface CompileOnSaveMixin { + /** + * If compile on save is enabled for the project + */ + compileOnSave?: boolean; + } + /** + * For external projects, some of the project settings are sent together with + * compiler settings. + */ + type ExternalProjectCompilerOptions = CompilerOptions & CompileOnSaveMixin; + /** + * Represents a set of changes that happen in project + */ + interface ProjectChanges { + /** + * List of added files + */ + added: string[]; + /** + * List of removed files + */ + removed: string[]; + /** + * List of updated files + */ + updated: string[]; + } + /** + * Information found in a configure request. + */ + interface ConfigureRequestArguments { + /** + * Information about the host, for example 'Emacs 24.4' or + * 'Sublime Text version 3075' + */ + hostInfo?: string; + /** + * If present, tab settings apply only to this file. + */ + file?: string; + /** + * The format options to use during formatting and other code editing features. + */ + formatOptions?: FormatCodeSettings; + preferences?: UserPreferences; + /** + * The host's additional supported .js file extensions + */ + extraFileExtensions?: FileExtensionInfo[]; + } + /** + * Configure request; value of command field is "configure". Specifies + * host information, such as host type, tab size, and indent size. + */ + interface ConfigureRequest extends Request { + command: CommandTypes.Configure; + arguments: ConfigureRequestArguments; + } + /** + * Response to "configure" request. This is just an acknowledgement, so + * no body field is required. + */ + interface ConfigureResponse extends Response { + } + /** + * Information found in an "open" request. + */ + interface OpenRequestArgs extends FileRequestArgs { + /** + * Used when a version of the file content is known to be more up to date than the one on disk. + * Then the known content will be used upon opening instead of the disk copy + */ + fileContent?: string; + /** + * Used to specify the script kind of the file explicitly. It could be one of the following: + * "TS", "JS", "TSX", "JSX" + */ + scriptKindName?: ScriptKindName; + /** + * Used to limit the searching for project config file. If given the searching will stop at this + * root path; otherwise it will go all the way up to the dist root path. + */ + projectRootPath?: string; + } + type ScriptKindName = "TS" | "JS" | "TSX" | "JSX"; + /** + * Open request; value of command field is "open". Notify the + * server that the client has file open. The server will not + * monitor the filesystem for changes in this file and will assume + * that the client is updating the server (using the change and/or + * reload messages) when the file changes. Server does not currently + * send a response to an open request. + */ + interface OpenRequest extends Request { + command: CommandTypes.Open; + arguments: OpenRequestArgs; + } + /** + * Request to open or update external project + */ + interface OpenExternalProjectRequest extends Request { + command: CommandTypes.OpenExternalProject; + arguments: OpenExternalProjectArgs; + } + /** + * Arguments to OpenExternalProjectRequest request + */ + type OpenExternalProjectArgs = ExternalProject; + /** + * Request to open multiple external projects + */ + interface OpenExternalProjectsRequest extends Request { + command: CommandTypes.OpenExternalProjects; + arguments: OpenExternalProjectsArgs; + } + /** + * Arguments to OpenExternalProjectsRequest + */ + interface OpenExternalProjectsArgs { + /** + * List of external projects to open or update + */ + projects: ExternalProject[]; + } + /** + * Response to OpenExternalProjectRequest request. This is just an acknowledgement, so + * no body field is required. + */ + interface OpenExternalProjectResponse extends Response { + } + /** + * Response to OpenExternalProjectsRequest request. This is just an acknowledgement, so + * no body field is required. + */ + interface OpenExternalProjectsResponse extends Response { + } + /** + * Request to close external project. + */ + interface CloseExternalProjectRequest extends Request { + command: CommandTypes.CloseExternalProject; + arguments: CloseExternalProjectRequestArgs; + } + /** + * Arguments to CloseExternalProjectRequest request + */ + interface CloseExternalProjectRequestArgs { + /** + * Name of the project to close + */ + projectFileName: string; + } + /** + * Response to CloseExternalProjectRequest request. This is just an acknowledgement, so + * no body field is required. + */ + interface CloseExternalProjectResponse extends Response { + } + /** + * Request to set compiler options for inferred projects. + * External projects are opened / closed explicitly. + * Configured projects are opened when user opens loose file that has 'tsconfig.json' or 'jsconfig.json' anywhere in one of containing folders. + * This configuration file will be used to obtain a list of files and configuration settings for the project. + * Inferred projects are created when user opens a loose file that is not the part of external project + * or configured project and will contain only open file and transitive closure of referenced files if 'useOneInferredProject' is false, + * or all open loose files and its transitive closure of referenced files if 'useOneInferredProject' is true. + */ + interface SetCompilerOptionsForInferredProjectsRequest extends Request { + command: CommandTypes.CompilerOptionsForInferredProjects; + arguments: SetCompilerOptionsForInferredProjectsArgs; + } + /** + * Argument for SetCompilerOptionsForInferredProjectsRequest request. + */ + interface SetCompilerOptionsForInferredProjectsArgs { + /** + * Compiler options to be used with inferred projects. + */ + options: ExternalProjectCompilerOptions; + /** + * Specifies the project root path used to scope compiler options. + * It is an error to provide this property if the server has not been started with + * `useInferredProjectPerProjectRoot` enabled. + */ + projectRootPath?: string; + } + /** + * Response to SetCompilerOptionsForInferredProjectsResponse request. This is just an acknowledgement, so + * no body field is required. + */ + interface SetCompilerOptionsForInferredProjectsResponse extends Response { + } + /** + * Exit request; value of command field is "exit". Ask the server process + * to exit. + */ + interface ExitRequest extends Request { + command: CommandTypes.Exit; + } + /** + * Close request; value of command field is "close". Notify the + * server that the client has closed a previously open file. If + * file is still referenced by open files, the server will resume + * monitoring the filesystem for changes to file. Server does not + * currently send a response to a close request. + */ + interface CloseRequest extends FileRequest { + command: CommandTypes.Close; + } + /** + * Request to obtain the list of files that should be regenerated if target file is recompiled. + * NOTE: this us query-only operation and does not generate any output on disk. + */ + interface CompileOnSaveAffectedFileListRequest extends FileRequest { + command: CommandTypes.CompileOnSaveAffectedFileList; + } + /** + * Contains a list of files that should be regenerated in a project + */ + interface CompileOnSaveAffectedFileListSingleProject { + /** + * Project name + */ + projectFileName: string; + /** + * List of files names that should be recompiled + */ + fileNames: string[]; + /** + * true if project uses outFile or out compiler option + */ + projectUsesOutFile: boolean; + } + /** + * Response for CompileOnSaveAffectedFileListRequest request; + */ + interface CompileOnSaveAffectedFileListResponse extends Response { + body: CompileOnSaveAffectedFileListSingleProject[]; + } + /** + * Request to recompile the file. All generated outputs (.js, .d.ts or .js.map files) is written on disk. + */ + interface CompileOnSaveEmitFileRequest extends FileRequest { + command: CommandTypes.CompileOnSaveEmitFile; + arguments: CompileOnSaveEmitFileRequestArgs; + } + /** + * Arguments for CompileOnSaveEmitFileRequest + */ + interface CompileOnSaveEmitFileRequestArgs extends FileRequestArgs { + /** + * if true - then file should be recompiled even if it does not have any changes. + */ + forced?: boolean; + } + /** + * Quickinfo request; value of command field is + * "quickinfo". Return response giving a quick type and + * documentation string for the symbol found in file at location + * line, col. + */ + interface QuickInfoRequest extends FileLocationRequest { + command: CommandTypes.Quickinfo; + } + /** + * Body of QuickInfoResponse. + */ + interface QuickInfoResponseBody { + /** + * The symbol's kind (such as 'className' or 'parameterName' or plain 'text'). + */ + kind: ScriptElementKind; + /** + * Optional modifiers for the kind (such as 'public'). + */ + kindModifiers: string; + /** + * Starting file location of symbol. + */ + start: Location; + /** + * One past last character of symbol. + */ + end: Location; + /** + * Type and kind of symbol. + */ + displayString: string; + /** + * Documentation associated with symbol. + */ + documentation: string; + /** + * JSDoc tags associated with symbol. + */ + tags: JSDocTagInfo[]; + } + /** + * Quickinfo response message. + */ + interface QuickInfoResponse extends Response { + body?: QuickInfoResponseBody; + } + /** + * Arguments for format messages. + */ + interface FormatRequestArgs extends FileLocationRequestArgs { + /** + * Last line of range for which to format text in file. + */ + endLine: number; + /** + * Character offset on last line of range for which to format text in file. + */ + endOffset: number; + /** + * Format options to be used. + */ + options?: FormatCodeSettings; + } + /** + * Format request; value of command field is "format". Return + * response giving zero or more edit instructions. The edit + * instructions will be sorted in file order. Applying the edit + * instructions in reverse to file will result in correctly + * reformatted text. + */ + interface FormatRequest extends FileLocationRequest { + command: CommandTypes.Format; + arguments: FormatRequestArgs; + } + /** + * Object found in response messages defining an editing + * instruction for a span of text in source code. The effect of + * this instruction is to replace the text starting at start and + * ending one character before end with newText. For an insertion, + * the text span is empty. For a deletion, newText is empty. + */ + interface CodeEdit { + /** + * First character of the text span to edit. + */ + start: Location; + /** + * One character past last character of the text span to edit. + */ + end: Location; + /** + * Replace the span defined above with this string (may be + * the empty string). + */ + newText: string; + } + interface FileCodeEdits { + fileName: string; + textChanges: CodeEdit[]; + } + interface CodeFixResponse extends Response { + /** The code actions that are available */ + body?: CodeFixAction[]; + } + interface CodeAction { + /** Description of the code action to display in the UI of the editor */ + description: string; + /** Text changes to apply to each file as part of the code action */ + changes: FileCodeEdits[]; + /** A command is an opaque object that should be passed to `ApplyCodeActionCommandRequestArgs` without modification. */ + commands?: {}[]; + } + interface CombinedCodeActions { + changes: ReadonlyArray<FileCodeEdits>; + commands?: ReadonlyArray<{}>; + } + interface CodeFixAction extends CodeAction { + /** Short name to identify the fix, for use by telemetry. */ + fixName: string; + /** + * If present, one may call 'getCombinedCodeFix' with this fixId. + * This may be omitted to indicate that the code fix can't be applied in a group. + */ + fixId?: {}; + /** Should be present if and only if 'fixId' is. */ + fixAllDescription?: string; + } + /** + * Format and format on key response message. + */ + interface FormatResponse extends Response { + body?: CodeEdit[]; + } + /** + * Arguments for format on key messages. + */ + interface FormatOnKeyRequestArgs extends FileLocationRequestArgs { + /** + * Key pressed (';', '\n', or '}'). + */ + key: string; + options?: FormatCodeSettings; + } + /** + * Format on key request; value of command field is + * "formatonkey". Given file location and key typed (as string), + * return response giving zero or more edit instructions. The + * edit instructions will be sorted in file order. Applying the + * edit instructions in reverse to file will result in correctly + * reformatted text. + */ + interface FormatOnKeyRequest extends FileLocationRequest { + command: CommandTypes.Formatonkey; + arguments: FormatOnKeyRequestArgs; + } + type CompletionsTriggerCharacter = "." | '"' | "'" | "`" | "/" | "@" | "<"; + /** + * Arguments for completions messages. + */ + interface CompletionsRequestArgs extends FileLocationRequestArgs { + /** + * Optional prefix to apply to possible completions. + */ + prefix?: string; + /** + * Character that was responsible for triggering completion. + * Should be `undefined` if a user manually requested completion. + */ + triggerCharacter?: CompletionsTriggerCharacter; + /** + * @deprecated Use UserPreferences.includeCompletionsForModuleExports + */ + includeExternalModuleExports?: boolean; + /** + * @deprecated Use UserPreferences.includeCompletionsWithInsertText + */ + includeInsertTextCompletions?: boolean; + } + /** + * Completions request; value of command field is "completions". + * Given a file location (file, line, col) and a prefix (which may + * be the empty string), return the possible completions that + * begin with prefix. + */ + interface CompletionsRequest extends FileLocationRequest { + command: CommandTypes.Completions; + arguments: CompletionsRequestArgs; + } + /** + * Arguments for completion details request. + */ + interface CompletionDetailsRequestArgs extends FileLocationRequestArgs { + /** + * Names of one or more entries for which to obtain details. + */ + entryNames: (string | CompletionEntryIdentifier)[]; + } + interface CompletionEntryIdentifier { + name: string; + source?: string; + } + /** + * Completion entry details request; value of command field is + * "completionEntryDetails". Given a file location (file, line, + * col) and an array of completion entry names return more + * detailed information for each completion entry. + */ + interface CompletionDetailsRequest extends FileLocationRequest { + command: CommandTypes.CompletionDetails; + arguments: CompletionDetailsRequestArgs; + } + /** + * Part of a symbol description. + */ + interface SymbolDisplayPart { + /** + * Text of an item describing the symbol. + */ + text: string; + /** + * The symbol's kind (such as 'className' or 'parameterName' or plain 'text'). + */ + kind: string; + } + /** + * An item found in a completion response. + */ + interface CompletionEntry { + /** + * The symbol's name. + */ + name: string; + /** + * The symbol's kind (such as 'className' or 'parameterName'). + */ + kind: ScriptElementKind; + /** + * Optional modifiers for the kind (such as 'public'). + */ + kindModifiers?: string; + /** + * A string that is used for comparing completion items so that they can be ordered. This + * is often the same as the name but may be different in certain circumstances. + */ + sortText: string; + /** + * Text to insert instead of `name`. + * This is used to support bracketed completions; If `name` might be "a-b" but `insertText` would be `["a-b"]`, + * coupled with `replacementSpan` to replace a dotted access with a bracket access. + */ + insertText?: string; + /** + * An optional span that indicates the text to be replaced by this completion item. + * If present, this span should be used instead of the default one. + * It will be set if the required span differs from the one generated by the default replacement behavior. + */ + replacementSpan?: TextSpan; + /** + * Indicates whether commiting this completion entry will require additional code actions to be + * made to avoid errors. The CompletionEntryDetails will have these actions. + */ + hasAction?: true; + /** + * Identifier (not necessarily human-readable) identifying where this completion came from. + */ + source?: string; + /** + * If true, this completion should be highlighted as recommended. There will only be one of these. + * This will be set when we know the user should write an expression with a certain type and that type is an enum or constructable class. + * Then either that enum/class or a namespace containing it will be the recommended symbol. + */ + isRecommended?: true; + } + /** + * Additional completion entry details, available on demand + */ + interface CompletionEntryDetails { + /** + * The symbol's name. + */ + name: string; + /** + * The symbol's kind (such as 'className' or 'parameterName'). + */ + kind: ScriptElementKind; + /** + * Optional modifiers for the kind (such as 'public'). + */ + kindModifiers: string; + /** + * Display parts of the symbol (similar to quick info). + */ + displayParts: SymbolDisplayPart[]; + /** + * Documentation strings for the symbol. + */ + documentation?: SymbolDisplayPart[]; + /** + * JSDoc tags for the symbol. + */ + tags?: JSDocTagInfo[]; + /** + * The associated code actions for this entry + */ + codeActions?: CodeAction[]; + /** + * Human-readable description of the `source` from the CompletionEntry. + */ + source?: SymbolDisplayPart[]; + } + /** @deprecated Prefer CompletionInfoResponse, which supports several top-level fields in addition to the array of entries. */ + interface CompletionsResponse extends Response { + body?: CompletionEntry[]; + } + interface CompletionInfoResponse extends Response { + body?: CompletionInfo; + } + interface CompletionInfo { + readonly isGlobalCompletion: boolean; + readonly isMemberCompletion: boolean; + readonly isNewIdentifierLocation: boolean; + readonly entries: ReadonlyArray<CompletionEntry>; + } + interface CompletionDetailsResponse extends Response { + body?: CompletionEntryDetails[]; + } + /** + * Signature help information for a single parameter + */ + interface SignatureHelpParameter { + /** + * The parameter's name + */ + name: string; + /** + * Documentation of the parameter. + */ + documentation: SymbolDisplayPart[]; + /** + * Display parts of the parameter. + */ + displayParts: SymbolDisplayPart[]; + /** + * Whether the parameter is optional or not. + */ + isOptional: boolean; + } + /** + * Represents a single signature to show in signature help. + */ + interface SignatureHelpItem { + /** + * Whether the signature accepts a variable number of arguments. + */ + isVariadic: boolean; + /** + * The prefix display parts. + */ + prefixDisplayParts: SymbolDisplayPart[]; + /** + * The suffix display parts. + */ + suffixDisplayParts: SymbolDisplayPart[]; + /** + * The separator display parts. + */ + separatorDisplayParts: SymbolDisplayPart[]; + /** + * The signature helps items for the parameters. + */ + parameters: SignatureHelpParameter[]; + /** + * The signature's documentation + */ + documentation: SymbolDisplayPart[]; + /** + * The signature's JSDoc tags + */ + tags: JSDocTagInfo[]; + } + /** + * Signature help items found in the response of a signature help request. + */ + interface SignatureHelpItems { + /** + * The signature help items. + */ + items: SignatureHelpItem[]; + /** + * The span for which signature help should appear on a signature + */ + applicableSpan: TextSpan; + /** + * The item selected in the set of available help items. + */ + selectedItemIndex: number; + /** + * The argument selected in the set of parameters. + */ + argumentIndex: number; + /** + * The argument count + */ + argumentCount: number; + } + type SignatureHelpTriggerCharacter = "," | "(" | "<"; + type SignatureHelpRetriggerCharacter = SignatureHelpTriggerCharacter | ")"; + /** + * Arguments of a signature help request. + */ + interface SignatureHelpRequestArgs extends FileLocationRequestArgs { + /** + * Reason why signature help was invoked. + * See each individual possible + */ + triggerReason?: SignatureHelpTriggerReason; + } + type SignatureHelpTriggerReason = SignatureHelpInvokedReason | SignatureHelpCharacterTypedReason | SignatureHelpRetriggeredReason; + /** + * Signals that the user manually requested signature help. + * The language service will unconditionally attempt to provide a result. + */ + interface SignatureHelpInvokedReason { + kind: "invoked"; + triggerCharacter?: undefined; + } + /** + * Signals that the signature help request came from a user typing a character. + * Depending on the character and the syntactic context, the request may or may not be served a result. + */ + interface SignatureHelpCharacterTypedReason { + kind: "characterTyped"; + /** + * Character that was responsible for triggering signature help. + */ + triggerCharacter: SignatureHelpTriggerCharacter; + } + /** + * Signals that this signature help request came from typing a character or moving the cursor. + * This should only occur if a signature help session was already active and the editor needs to see if it should adjust. + * The language service will unconditionally attempt to provide a result. + * `triggerCharacter` can be `undefined` for a retrigger caused by a cursor move. + */ + interface SignatureHelpRetriggeredReason { + kind: "retrigger"; + /** + * Character that was responsible for triggering signature help. + */ + triggerCharacter?: SignatureHelpRetriggerCharacter; + } + /** + * Signature help request; value of command field is "signatureHelp". + * Given a file location (file, line, col), return the signature + * help. + */ + interface SignatureHelpRequest extends FileLocationRequest { + command: CommandTypes.SignatureHelp; + arguments: SignatureHelpRequestArgs; + } + /** + * Response object for a SignatureHelpRequest. + */ + interface SignatureHelpResponse extends Response { + body?: SignatureHelpItems; + } + /** + * Synchronous request for semantic diagnostics of one file. + */ + interface SemanticDiagnosticsSyncRequest extends FileRequest { + command: CommandTypes.SemanticDiagnosticsSync; + arguments: SemanticDiagnosticsSyncRequestArgs; + } + interface SemanticDiagnosticsSyncRequestArgs extends FileRequestArgs { + includeLinePosition?: boolean; + } + /** + * Response object for synchronous sematic diagnostics request. + */ + interface SemanticDiagnosticsSyncResponse extends Response { + body?: Diagnostic[] | DiagnosticWithLinePosition[]; + } + interface SuggestionDiagnosticsSyncRequest extends FileRequest { + command: CommandTypes.SuggestionDiagnosticsSync; + arguments: SuggestionDiagnosticsSyncRequestArgs; + } + type SuggestionDiagnosticsSyncRequestArgs = SemanticDiagnosticsSyncRequestArgs; + type SuggestionDiagnosticsSyncResponse = SemanticDiagnosticsSyncResponse; + /** + * Synchronous request for syntactic diagnostics of one file. + */ + interface SyntacticDiagnosticsSyncRequest extends FileRequest { + command: CommandTypes.SyntacticDiagnosticsSync; + arguments: SyntacticDiagnosticsSyncRequestArgs; + } + interface SyntacticDiagnosticsSyncRequestArgs extends FileRequestArgs { + includeLinePosition?: boolean; + } + /** + * Response object for synchronous syntactic diagnostics request. + */ + interface SyntacticDiagnosticsSyncResponse extends Response { + body?: Diagnostic[] | DiagnosticWithLinePosition[]; + } + /** + * Arguments for GeterrForProject request. + */ + interface GeterrForProjectRequestArgs { + /** + * the file requesting project error list + */ + file: string; + /** + * Delay in milliseconds to wait before starting to compute + * errors for the files in the file list + */ + delay: number; + } + /** + * GeterrForProjectRequest request; value of command field is + * "geterrForProject". It works similarly with 'Geterr', only + * it request for every file in this project. + */ + interface GeterrForProjectRequest extends Request { + command: CommandTypes.GeterrForProject; + arguments: GeterrForProjectRequestArgs; + } + /** + * Arguments for geterr messages. + */ + interface GeterrRequestArgs { + /** + * List of file names for which to compute compiler errors. + * The files will be checked in list order. + */ + files: string[]; + /** + * Delay in milliseconds to wait before starting to compute + * errors for the files in the file list + */ + delay: number; + } + /** + * Geterr request; value of command field is "geterr". Wait for + * delay milliseconds and then, if during the wait no change or + * reload messages have arrived for the first file in the files + * list, get the syntactic errors for the file, field requests, + * and then get the semantic errors for the file. Repeat with a + * smaller delay for each subsequent file on the files list. Best + * practice for an editor is to send a file list containing each + * file that is currently visible, in most-recently-used order. + */ + interface GeterrRequest extends Request { + command: CommandTypes.Geterr; + arguments: GeterrRequestArgs; + } + type RequestCompletedEventName = "requestCompleted"; + /** + * Event that is sent when server have finished processing request with specified id. + */ + interface RequestCompletedEvent extends Event { + event: RequestCompletedEventName; + body: RequestCompletedEventBody; + } + interface RequestCompletedEventBody { + request_seq: number; + } + /** + * Item of diagnostic information found in a DiagnosticEvent message. + */ + interface Diagnostic { + /** + * Starting file location at which text applies. + */ + start: Location; + /** + * The last file location at which the text applies. + */ + end: Location; + /** + * Text of diagnostic message. + */ + text: string; + /** + * The category of the diagnostic message, e.g. "error", "warning", or "suggestion". + */ + category: string; + reportsUnnecessary?: {}; + /** + * Any related spans the diagnostic may have, such as other locations relevant to an error, such as declarartion sites + */ + relatedInformation?: DiagnosticRelatedInformation[]; + /** + * The error code of the diagnostic message. + */ + code?: number; + /** + * The name of the plugin reporting the message. + */ + source?: string; + } + interface DiagnosticWithFileName extends Diagnostic { + /** + * Name of the file the diagnostic is in + */ + fileName: string; + } + /** + * Represents additional spans returned with a diagnostic which are relevant to it + */ + interface DiagnosticRelatedInformation { + /** + * The category of the related information message, e.g. "error", "warning", or "suggestion". + */ + category: string; + /** + * The code used ot identify the related information + */ + code: number; + /** + * Text of related or additional information. + */ + message: string; + /** + * Associated location + */ + span?: FileSpan; + } + interface DiagnosticEventBody { + /** + * The file for which diagnostic information is reported. + */ + file: string; + /** + * An array of diagnostic information items. + */ + diagnostics: Diagnostic[]; + } + type DiagnosticEventKind = "semanticDiag" | "syntaxDiag" | "suggestionDiag"; + /** + * Event message for DiagnosticEventKind event types. + * These events provide syntactic and semantic errors for a file. + */ + interface DiagnosticEvent extends Event { + body?: DiagnosticEventBody; + } + interface ConfigFileDiagnosticEventBody { + /** + * The file which trigged the searching and error-checking of the config file + */ + triggerFile: string; + /** + * The name of the found config file. + */ + configFile: string; + /** + * An arry of diagnostic information items for the found config file. + */ + diagnostics: DiagnosticWithFileName[]; + } + /** + * Event message for "configFileDiag" event type. + * This event provides errors for a found config file. + */ + interface ConfigFileDiagnosticEvent extends Event { + body?: ConfigFileDiagnosticEventBody; + event: "configFileDiag"; + } + type ProjectLanguageServiceStateEventName = "projectLanguageServiceState"; + interface ProjectLanguageServiceStateEvent extends Event { + event: ProjectLanguageServiceStateEventName; + body?: ProjectLanguageServiceStateEventBody; + } + interface ProjectLanguageServiceStateEventBody { + /** + * Project name that has changes in the state of language service. + * For configured projects this will be the config file path. + * For external projects this will be the name of the projects specified when project was open. + * For inferred projects this event is not raised. + */ + projectName: string; + /** + * True if language service state switched from disabled to enabled + * and false otherwise. + */ + languageServiceEnabled: boolean; + } + type ProjectsUpdatedInBackgroundEventName = "projectsUpdatedInBackground"; + interface ProjectsUpdatedInBackgroundEvent extends Event { + event: ProjectsUpdatedInBackgroundEventName; + body: ProjectsUpdatedInBackgroundEventBody; + } + interface ProjectsUpdatedInBackgroundEventBody { + /** + * Current set of open files + */ + openFiles: string[]; + } + /** + * Arguments for reload request. + */ + interface ReloadRequestArgs extends FileRequestArgs { + /** + * Name of temporary file from which to reload file + * contents. May be same as file. + */ + tmpfile: string; + } + /** + * Reload request message; value of command field is "reload". + * Reload contents of file with name given by the 'file' argument + * from temporary file with name given by the 'tmpfile' argument. + * The two names can be identical. + */ + interface ReloadRequest extends FileRequest { + command: CommandTypes.Reload; + arguments: ReloadRequestArgs; + } + /** + * Response to "reload" request. This is just an acknowledgement, so + * no body field is required. + */ + interface ReloadResponse extends Response { + } + /** + * Arguments for saveto request. + */ + interface SavetoRequestArgs extends FileRequestArgs { + /** + * Name of temporary file into which to save server's view of + * file contents. + */ + tmpfile: string; + } + /** + * Saveto request message; value of command field is "saveto". + * For debugging purposes, save to a temporaryfile (named by + * argument 'tmpfile') the contents of file named by argument + * 'file'. The server does not currently send a response to a + * "saveto" request. + */ + interface SavetoRequest extends FileRequest { + command: CommandTypes.Saveto; + arguments: SavetoRequestArgs; + } + /** + * Arguments for navto request message. + */ + interface NavtoRequestArgs extends FileRequestArgs { + /** + * Search term to navigate to from current location; term can + * be '.*' or an identifier prefix. + */ + searchValue: string; + /** + * Optional limit on the number of items to return. + */ + maxResultCount?: number; + /** + * Optional flag to indicate we want results for just the current file + * or the entire project. + */ + currentFileOnly?: boolean; + projectFileName?: string; + } + /** + * Navto request message; value of command field is "navto". + * Return list of objects giving file locations and symbols that + * match the search term given in argument 'searchTerm'. The + * context for the search is given by the named file. + */ + interface NavtoRequest extends FileRequest { + command: CommandTypes.Navto; + arguments: NavtoRequestArgs; + } + /** + * An item found in a navto response. + */ + interface NavtoItem extends FileSpan { + /** + * The symbol's name. + */ + name: string; + /** + * The symbol's kind (such as 'className' or 'parameterName'). + */ + kind: ScriptElementKind; + /** + * exact, substring, or prefix. + */ + matchKind: string; + /** + * If this was a case sensitive or insensitive match. + */ + isCaseSensitive: boolean; + /** + * Optional modifiers for the kind (such as 'public'). + */ + kindModifiers?: string; + /** + * Name of symbol's container symbol (if any); for example, + * the class name if symbol is a class member. + */ + containerName?: string; + /** + * Kind of symbol's container symbol (if any). + */ + containerKind?: ScriptElementKind; + } + /** + * Navto response message. Body is an array of navto items. Each + * item gives a symbol that matched the search term. + */ + interface NavtoResponse extends Response { + body?: NavtoItem[]; + } + /** + * Arguments for change request message. + */ + interface ChangeRequestArgs extends FormatRequestArgs { + /** + * Optional string to insert at location (file, line, offset). + */ + insertString?: string; + } + /** + * Change request message; value of command field is "change". + * Update the server's view of the file named by argument 'file'. + * Server does not currently send a response to a change request. + */ + interface ChangeRequest extends FileLocationRequest { + command: CommandTypes.Change; + arguments: ChangeRequestArgs; + } + /** + * Response to "brace" request. + */ + interface BraceResponse extends Response { + body?: TextSpan[]; + } + /** + * Brace matching request; value of command field is "brace". + * Return response giving the file locations of matching braces + * found in file at location line, offset. + */ + interface BraceRequest extends FileLocationRequest { + command: CommandTypes.Brace; + } + /** + * NavBar items request; value of command field is "navbar". + * Return response giving the list of navigation bar entries + * extracted from the requested file. + */ + interface NavBarRequest extends FileRequest { + command: CommandTypes.NavBar; + } + /** + * NavTree request; value of command field is "navtree". + * Return response giving the navigation tree of the requested file. + */ + interface NavTreeRequest extends FileRequest { + command: CommandTypes.NavTree; + } + interface NavigationBarItem { + /** + * The item's display text. + */ + text: string; + /** + * The symbol's kind (such as 'className' or 'parameterName'). + */ + kind: ScriptElementKind; + /** + * Optional modifiers for the kind (such as 'public'). + */ + kindModifiers?: string; + /** + * The definition locations of the item. + */ + spans: TextSpan[]; + /** + * Optional children. + */ + childItems?: NavigationBarItem[]; + /** + * Number of levels deep this item should appear. + */ + indent: number; + } + /** protocol.NavigationTree is identical to ts.NavigationTree, except using protocol.TextSpan instead of ts.TextSpan */ + interface NavigationTree { + text: string; + kind: ScriptElementKind; + kindModifiers: string; + spans: TextSpan[]; + nameSpan: TextSpan | undefined; + childItems?: NavigationTree[]; + } + type TelemetryEventName = "telemetry"; + interface TelemetryEvent extends Event { + event: TelemetryEventName; + body: TelemetryEventBody; + } + interface TelemetryEventBody { + telemetryEventName: string; + payload: any; + } + type TypesInstallerInitializationFailedEventName = "typesInstallerInitializationFailed"; + interface TypesInstallerInitializationFailedEvent extends Event { + event: TypesInstallerInitializationFailedEventName; + body: TypesInstallerInitializationFailedEventBody; + } + interface TypesInstallerInitializationFailedEventBody { + message: string; + } + type TypingsInstalledTelemetryEventName = "typingsInstalled"; + interface TypingsInstalledTelemetryEventBody extends TelemetryEventBody { + telemetryEventName: TypingsInstalledTelemetryEventName; + payload: TypingsInstalledTelemetryEventPayload; + } + interface TypingsInstalledTelemetryEventPayload { + /** + * Comma separated list of installed typing packages + */ + installedPackages: string; + /** + * true if install request succeeded, otherwise - false + */ + installSuccess: boolean; + /** + * version of typings installer + */ + typingsInstallerVersion: string; + } + type BeginInstallTypesEventName = "beginInstallTypes"; + type EndInstallTypesEventName = "endInstallTypes"; + interface BeginInstallTypesEvent extends Event { + event: BeginInstallTypesEventName; + body: BeginInstallTypesEventBody; + } + interface EndInstallTypesEvent extends Event { + event: EndInstallTypesEventName; + body: EndInstallTypesEventBody; + } + interface InstallTypesEventBody { + /** + * correlation id to match begin and end events + */ + eventId: number; + /** + * list of packages to install + */ + packages: ReadonlyArray<string>; + } + interface BeginInstallTypesEventBody extends InstallTypesEventBody { + } + interface EndInstallTypesEventBody extends InstallTypesEventBody { + /** + * true if installation succeeded, otherwise false + */ + success: boolean; + } + interface NavBarResponse extends Response { + body?: NavigationBarItem[]; + } + interface NavTreeResponse extends Response { + body?: NavigationTree; + } + const enum IndentStyle { + None = "None", + Block = "Block", + Smart = "Smart" + } + interface EditorSettings { + baseIndentSize?: number; + indentSize?: number; + tabSize?: number; + newLineCharacter?: string; + convertTabsToSpaces?: boolean; + indentStyle?: IndentStyle | ts.IndentStyle; + } + interface FormatCodeSettings extends EditorSettings { + insertSpaceAfterCommaDelimiter?: boolean; + insertSpaceAfterSemicolonInForStatements?: boolean; + insertSpaceBeforeAndAfterBinaryOperators?: boolean; + insertSpaceAfterConstructor?: boolean; + insertSpaceAfterKeywordsInControlFlowStatements?: boolean; + insertSpaceAfterFunctionKeywordForAnonymousFunctions?: boolean; + insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis?: boolean; + insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets?: boolean; + insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces?: boolean; + insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces?: boolean; + insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces?: boolean; + insertSpaceAfterTypeAssertion?: boolean; + insertSpaceBeforeFunctionParenthesis?: boolean; + placeOpenBraceOnNewLineForFunctions?: boolean; + placeOpenBraceOnNewLineForControlBlocks?: boolean; + insertSpaceBeforeTypeAnnotation?: boolean; + } + interface UserPreferences { + readonly disableSuggestions?: boolean; + readonly quotePreference?: "double" | "single"; + /** + * If enabled, TypeScript will search through all external modules' exports and add them to the completions list. + * This affects lone identifier completions but not completions on the right hand side of `obj.`. + */ + readonly includeCompletionsForModuleExports?: boolean; + /** + * If enabled, the completion list will include completions with invalid identifier names. + * For those entries, The `insertText` and `replacementSpan` properties will be set to change from `.x` property access to `["x"]`. + */ + readonly includeCompletionsWithInsertText?: boolean; + readonly importModuleSpecifierPreference?: "relative" | "non-relative"; + readonly allowTextChangesInNewFiles?: boolean; + } + interface CompilerOptions { + allowJs?: boolean; + allowSyntheticDefaultImports?: boolean; + allowUnreachableCode?: boolean; + allowUnusedLabels?: boolean; + alwaysStrict?: boolean; + baseUrl?: string; + charset?: string; + checkJs?: boolean; + declaration?: boolean; + declarationDir?: string; + disableSizeLimit?: boolean; + downlevelIteration?: boolean; + emitBOM?: boolean; + emitDecoratorMetadata?: boolean; + experimentalDecorators?: boolean; + forceConsistentCasingInFileNames?: boolean; + importHelpers?: boolean; + inlineSourceMap?: boolean; + inlineSources?: boolean; + isolatedModules?: boolean; + jsx?: JsxEmit | ts.JsxEmit; + lib?: string[]; + locale?: string; + mapRoot?: string; + maxNodeModuleJsDepth?: number; + module?: ModuleKind | ts.ModuleKind; + moduleResolution?: ModuleResolutionKind | ts.ModuleResolutionKind; + newLine?: NewLineKind | ts.NewLineKind; + noEmit?: boolean; + noEmitHelpers?: boolean; + noEmitOnError?: boolean; + noErrorTruncation?: boolean; + noFallthroughCasesInSwitch?: boolean; + noImplicitAny?: boolean; + noImplicitReturns?: boolean; + noImplicitThis?: boolean; + noUnusedLocals?: boolean; + noUnusedParameters?: boolean; + noImplicitUseStrict?: boolean; + noLib?: boolean; + noResolve?: boolean; + out?: string; + outDir?: string; + outFile?: string; + paths?: MapLike<string[]>; + plugins?: PluginImport[]; + preserveConstEnums?: boolean; + preserveSymlinks?: boolean; + project?: string; + reactNamespace?: string; + removeComments?: boolean; + references?: ProjectReference[]; + rootDir?: string; + rootDirs?: string[]; + skipLibCheck?: boolean; + skipDefaultLibCheck?: boolean; + sourceMap?: boolean; + sourceRoot?: string; + strict?: boolean; + strictNullChecks?: boolean; + suppressExcessPropertyErrors?: boolean; + suppressImplicitAnyIndexErrors?: boolean; + target?: ScriptTarget | ts.ScriptTarget; + traceResolution?: boolean; + resolveJsonModule?: boolean; + types?: string[]; + /** Paths used to used to compute primary types search locations */ + typeRoots?: string[]; + [option: string]: CompilerOptionsValue | undefined; + } + const enum JsxEmit { + None = "None", + Preserve = "Preserve", + ReactNative = "ReactNative", + React = "React" + } + const enum ModuleKind { + None = "None", + CommonJS = "CommonJS", + AMD = "AMD", + UMD = "UMD", + System = "System", + ES6 = "ES6", + ES2015 = "ES2015", + ESNext = "ESNext" + } + const enum ModuleResolutionKind { + Classic = "Classic", + Node = "Node" + } + const enum NewLineKind { + Crlf = "Crlf", + Lf = "Lf" + } + const enum ScriptTarget { + ES3 = "ES3", + ES5 = "ES5", + ES6 = "ES6", + ES2015 = "ES2015", + ES2016 = "ES2016", + ES2017 = "ES2017", + ESNext = "ESNext" + } +} +declare namespace ts.server.protocol { + + interface TextInsertion { + newText: string; + /** The position in newText the caret should point to after the insertion. */ + caretOffset: number; + } + + interface TodoCommentDescriptor { + text: string; + priority: number; + } + + interface TodoComment { + descriptor: TodoCommentDescriptor; + message: string; + position: number; + } + + enum OutliningSpanKind { + /** Single or multi-line comments */ + Comment = "comment", + /** Sections marked by '// #region' and '// #endregion' comments */ + Region = "region", + /** Declarations and expressions */ + Code = "code", + /** Contiguous blocks of import declarations */ + Imports = "imports" + } + + enum HighlightSpanKind { + none = "none", + definition = "definition", + reference = "reference", + writtenReference = "writtenReference" + } + + enum ScriptElementKind { + unknown = "", + warning = "warning", + /** predefined type (void) or keyword (class) */ + keyword = "keyword", + /** top level script node */ + scriptElement = "script", + /** module foo {} */ + moduleElement = "module", + /** class X {} */ + classElement = "class", + /** var x = class X {} */ + localClassElement = "local class", + /** interface Y {} */ + interfaceElement = "interface", + /** type T = ... */ + typeElement = "type", + /** enum E */ + enumElement = "enum", + enumMemberElement = "enum member", + /** + * Inside module and script only + * const v = .. + */ + variableElement = "var", + /** Inside function */ + localVariableElement = "local var", + /** + * Inside module and script only + * function f() { } + */ + functionElement = "function", + /** Inside function */ + localFunctionElement = "local function", + /** class X { [public|private]* foo() {} } */ + memberFunctionElement = "method", + /** class X { [public|private]* [get|set] foo:number; } */ + memberGetAccessorElement = "getter", + memberSetAccessorElement = "setter", + /** + * class X { [public|private]* foo:number; } + * interface Y { foo:number; } + */ + memberVariableElement = "property", + /** class X { constructor() { } } */ + constructorImplementationElement = "constructor", + /** interface Y { ():number; } */ + callSignatureElement = "call", + /** interface Y { []:number; } */ + indexSignatureElement = "index", + /** interface Y { new():Y; } */ + constructSignatureElement = "construct", + /** function foo(*Y*: string) */ + parameterElement = "parameter", + typeParameterElement = "type parameter", + primitiveType = "primitive type", + label = "label", + alias = "alias", + constElement = "const", + letElement = "let", + directory = "directory", + externalModuleName = "external module name", + /** + * <JsxTagName attribute1 attribute2={0} /> + */ + jsxAttribute = "JSX attribute", + /** String literal */ + string = "string" + } + + interface TypeAcquisition { + enableAutoDiscovery?: boolean; + enable?: boolean; + include?: string[]; + exclude?: string[]; + [option: string]: string[] | boolean | undefined; + } + + interface FileExtensionInfo { + extension: string; + isMixedContent: boolean; + scriptKind?: ScriptKind; + } + + interface JSDocTagInfo { + name: string; + text?: string; + } + + /** + * Type of objects whose values are all of the same type. + * The `in` and `for-in` operators can *not* be safely used, + * since `Object.prototype` may be modified by outside code. + */ + interface MapLike<T> { + [index: string]: T; + } + + interface PluginImport { + name: string; + } + + interface ProjectReference { + /** A normalized path on disk */ + path: string; + /** The path as the user originally wrote it */ + originalPath?: string; + /** True if the output of this reference should be prepended to the output of this project. Only valid for --outFile compilations */ + prepend?: boolean; + /** True if it is intended that this reference form a circularity */ + circular?: boolean; + } + + type CompilerOptionsValue = string | number | boolean | (string | number)[] | string[] | MapLike<string[]> | PluginImport[] | ProjectReference[] | null | undefined; +} +declare namespace ts { + // these types are empty stubs for types from services and should not be used directly + export type ScriptKind = never; + export type IndentStyle = never; + export type JsxEmit = never; + export type ModuleKind = never; + export type ModuleResolutionKind = never; + export type NewLineKind = never; + export type ScriptTarget = never; +} +import protocol = ts.server.protocol; +export = protocol; +export as namespace protocol; diff --git a/src/server/schema.json b/src/server/schema.json new file mode 100644 index 0000000..fb28acd --- /dev/null +++ b/src/server/schema.json @@ -0,0 +1,298 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema", + "properties": { + "tsserver.enable": { + "type": "boolean", + "default": true, + "description": "Enable tsserver extension" + }, + "tsserver.locale": { + "type": "string", + "default": "", + "description": "Locale of tsserver" + }, + "tsserver.typingsCacheLocation": { + "type": "string", + "default": "", + "description": "Folder path for cache typings" + }, + "tsserver.formatOnSave": { + "type": "boolean", + "default": false, + "description": "Format document on buffer will save" + }, + "tsserver.orgnizeImportOnSave": { + "type": "boolean", + "default": false, + "description": "Orgnize import on buffer will save" + }, + "tsserver.formatOnType": { + "type": "boolean", + "default": true, + "description": "Run format on type special characters." + }, + "tsserver.enableJavascript": { + "type": "boolean", + "default": true, + "description": "Use tsserver for javascript files" + }, + "tsserver.tsdk": { + "type": "string", + "default": "", + "description": "Directory contains tsserver.js, works for workspace only" + }, + "tsserver.npm": { + "type": "string", + "default": "", + "description": "Executable path of npm for download typings" + }, + "tsserver.log": { + "type": "string", + "default": "off", + "enum": ["normal", "terse", "verbose", "off"], + "description": "Log level of tsserver" + }, + "tsserver.trace.server": { + "type": "string", + "default": "off", + "enum": ["off", "messages", "verbose"], + "description": "Trace level of tsserver" + }, + "tserver.pluginNames": { + "type": "array", + "default": [], + "items": { + "type": "string" + }, + "description": "Module names of tsserver plugins" + }, + "tsserver.pluginRoot": { + "type": "string", + "default": "", + "description": "Folder contains tsserver plugins" + }, + "tsserver.debugPort": { + "type": "number", + "description": "Debug port number of tsserver" + }, + "tsserver.reportStyleChecksAsWarnings": { + "type": "boolean", + "default": true + }, + "tsserver.implicitProjectConfig.checkJs": { + "type": "boolean", + "default": false, + "description": "Enable checkJs for implicit project" + }, + "tsserver.implicitProjectConfig.experimentalDecorators": { + "type": "boolean", + "default": false, + "description": "Enable experimentalDecorators for implicit project" + }, + "tsserver.disableAutomaticTypeAcquisition": { + "type": "boolean", + "default": false, + "description": "Disable download of typings" + }, + "typescript.updateImportsOnFileMove.enable": { + "type": "boolean", + "default": true, + "description": "Enable update imports on file move." + }, + "typescript.implementationsCodeLens.enable": { + "type": "boolean", + "default": true, + "description": "Enable codeLens for implementations" + }, + "typescript.referencesCodeLens.enable": { + "type": "boolean", + "default": true, + "description": "Enable codeLens for references" + }, + "typescript.preferences.completion.useCodeSnippetsOnMethodSuggest": { + "type": "boolean", + "default": true, + "description": "Enable snippet for method suggestion" + }, + "typescript.preferences.completion.nameSuggestions": { + "type": "boolean", + "default": true, + "description": "Complete for warning type of tsserver" + }, + "typescript.preferences.completion.autoImportSuggestions": { + "type": "boolean", + "default": true, + "description": "Enable auto import suggestions for completion" + }, + "typescript.preferences.completion.commaAfterImport": { + "type": "boolean", + "default": true, + "description": "Add comma after import" + }, + "typescript.preferences.completion.moduleExports": { + "type": "boolean", + "default": true, + "description": "Include completion for module.exports" + }, + "typescript.preferences.importModuleSpecifier": { + "type": "string", + "default": "non-relative", + "enum": ["non-relative", "relative"] + }, + "typescript.preferences.suggestionActions.enabled": { + "type": "boolean", + "default": true + }, + "typescript.preferences.quoteStyle": { + "type": "string", + "default": "single", + "enum": ["single", "double"] + }, + "typescript.format.insertSpaceAfterCommaDelimiter": { + "type": "boolean", + "default": true + }, + "typescript.format.insertSpaceAfterConstructor": { + "type": "boolean", + "default": false + }, + "typescript.format.insertSpaceAfterSemicolonInForStatements": { + "type": "boolean", + "default": true + }, + "typescript.format.insertSpaceBeforeAndAfterBinaryOperators": { + "type": "boolean", + "default": true + }, + "typescript.format.insertSpaceAfterKeywordsInControlFlowStatements": { + "type": "boolean", + "default": true + }, + "typescript.format.insertSpaceAfterFunctionKeywordForAnonymousFunctions": { + "type": "boolean", + "default": true + }, + "typescript.format.insertSpaceBeforeFunctionParenthesis": { + "type": "boolean", + "default": false + }, + "typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis": { + "type": "boolean", + "default": false + }, + "typescript.format.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces": { + "type": "boolean", + "default": false + }, + "typescript.format.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces": { + "type": "boolean", + "default": false + }, + "typescript.format.insertSpaceAfterTypeAssertion": { + "type": "boolean", + "default": false + }, + "typescript.format.placeOpenBraceOnNewLineForFunctions": { + "type": "boolean", + "default": false + }, + "typescript.format.placeOpenBraceOnNewLineForControlBlocks": { + "type": "boolean", + "default": false + }, + "javascript.updateImportsOnFileMove.enable": { + "type": "boolean", + "default": true + }, + "javascript.implementationsCodeLens.enable": { + "type": "boolean", + "default": true + }, + "javascript.referencesCodeLens.enable": { + "type": "boolean", + "default": true + }, + "javascript.preferences.completion.useCodeSnippetsOnMethodSuggest": { + "type": "boolean", + "default": true + }, + "javascript.preferences.completion.nameSuggestions": { + "type": "boolean", + "default": true + }, + "javascript.preferences.completion.autoImportSuggestions": { + "type": "boolean", + "default": true + }, + "javascript.preferences.importModuleSpecifier": { + "type": "string", + "default": "non-relative", + "enum": ["non-relative", "relative"] + }, + "javascript.preferences.suggestionActions.enabled": { + "type": "boolean", + "default": true + }, + "javascript.preferences.completion.commaAfterImport": { + "type": "boolean", + "default": true + }, + "javascript.preferences.quoteStyle": { + "type": "string", + "default": "single", + "enum": ["single", "double"] + }, + "javascript.format.insertSpaceAfterCommaDelimiter": { + "type": "boolean", + "default": true + }, + "javascript.format.insertSpaceAfterConstructor": { + "type": "boolean", + "default": false + }, + "javascript.format.insertSpaceAfterSemicolonInForStatements": { + "type": "boolean", + "default": true + }, + "javascript.format.insertSpaceBeforeAndAfterBinaryOperators": { + "type": "boolean", + "default": true + }, + "javascript.format.insertSpaceAfterKeywordsInControlFlowStatements": { + "type": "boolean", + "default": true + }, + "javascript.format.insertSpaceAfterFunctionKeywordForAnonymousFunctions": { + "type": "boolean", + "default": true + }, + "javascript.format.insertSpaceBeforeFunctionParenthesis": { + "type": "boolean", + "default": false + }, + "javascript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis": { + "type": "boolean", + "default": false + }, + "javascript.format.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces": { + "type": "boolean", + "default": false + }, + "javascript.format.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces": { + "type": "boolean", + "default": false + }, + "javascript.format.insertSpaceAfterTypeAssertion": { + "type": "boolean", + "default": false + }, + "javascript.format.placeOpenBraceOnNewLineForFunctions": { + "type": "boolean", + "default": false + }, + "javascript.format.placeOpenBraceOnNewLineForControlBlocks": { + "type": "boolean", + "default": false + } + } +} diff --git a/src/server/typescriptService.ts b/src/server/typescriptService.ts new file mode 100644 index 0000000..a400012 --- /dev/null +++ b/src/server/typescriptService.ts @@ -0,0 +1,215 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { CancellationToken, Event } from 'vscode-languageserver-protocol' +import Uri from 'vscode-uri' +import * as Proto from './protocol' +import API from './utils/api' +import { TypeScriptServiceConfiguration } from './utils/configuration' +import Logger from './utils/logger' + +export interface TypeScriptServerPlugin { + readonly path: string + readonly name: string + readonly languages: string[] +} + +export interface ITypeScriptServiceClient { + apiVersion: API + configuration: TypeScriptServiceConfiguration + onTsServerStarted: Event<API> + onProjectLanguageServiceStateChanged: Event<Proto.ProjectLanguageServiceStateEventBody> + onDidBeginInstallTypings: Event<Proto.BeginInstallTypesEventBody> + onDidEndInstallTypings: Event<Proto.EndInstallTypesEventBody> + onTypesInstallerInitializationFailed: Event<Proto.TypesInstallerInitializationFailedEventBody> + readonly logger: Logger + + normalizePath(resource: Uri): string | null + asUrl(filepath: string): Uri + toPath(uri: string): string + toResource(path: string): string + + execute( + command: 'configure', + args: Proto.ConfigureRequestArguments, + token?: CancellationToken + ): Promise<Proto.ConfigureResponse> + execute( + command: 'open', + args: Proto.OpenRequestArgs, + expectedResult: boolean, + token?: CancellationToken + ): Promise<any> + execute( + command: 'close', + args: Proto.FileRequestArgs, + expectedResult: boolean, + token?: CancellationToken + ): Promise<any> + execute( + command: 'change', + args: Proto.ChangeRequestArgs, + expectedResult: boolean, + token?: CancellationToken + ): Promise<any> + execute( + command: 'geterr', + args: Proto.GeterrRequestArgs, + expectedResult: boolean, + token?: CancellationToken + ): Promise<any> + execute( + command: 'geterrForProject', + args: Proto.GeterrForProjectRequestArgs, + token?: CancellationToken + ): Promise<any> + execute( + command: 'quickinfo', + args: Proto.FileLocationRequestArgs, + token?: CancellationToken + ): Promise<Proto.QuickInfoResponse> + execute( + command: 'completions', + args: Proto.CompletionsRequestArgs, + token?: CancellationToken + ): Promise<Proto.CompletionsResponse> // tslint:disable-line + execute( + command: 'completionEntryDetails', + args: Proto.CompletionDetailsRequestArgs, + token?: CancellationToken + ): Promise<Proto.CompletionDetailsResponse> + execute( + command: 'signatureHelp', + args: Proto.SignatureHelpRequestArgs, + token?: CancellationToken + ): Promise<Proto.SignatureHelpResponse> + execute( + command: 'definition', + args: Proto.FileLocationRequestArgs, + token?: CancellationToken + ): Promise<Proto.DefinitionResponse> + execute( + command: 'implementation', + args: Proto.FileLocationRequestArgs, + token?: CancellationToken + ): Promise<Proto.ImplementationResponse> + execute( + command: 'typeDefinition', + args: Proto.FileLocationRequestArgs, + token?: CancellationToken + ): Promise<Proto.TypeDefinitionResponse> + execute( + command: 'references', + args: Proto.FileLocationRequestArgs, + token?: CancellationToken + ): Promise<Proto.ReferencesResponse> + execute( + command: 'navto', + args: Proto.NavtoRequestArgs, + token?: CancellationToken + ): Promise<Proto.NavtoResponse> + execute( + command: 'navbar', + args: Proto.FileRequestArgs, + token?: CancellationToken + ): Promise<Proto.NavBarResponse> + execute( + command: 'format', + args: Proto.FormatRequestArgs, + token?: CancellationToken + ): Promise<Proto.FormatResponse> + execute( + command: 'formatonkey', + args: Proto.FormatOnKeyRequestArgs, + token?: CancellationToken + ): Promise<Proto.FormatResponse> + execute( + command: 'rename', + args: Proto.RenameRequestArgs, + token?: CancellationToken + ): Promise<Proto.RenameResponse> + execute( + command: 'projectInfo', + args: Proto.ProjectInfoRequestArgs, + token?: CancellationToken + ): Promise<Proto.ProjectInfoResponse> + execute( + command: 'reloadProjects', + args: any, + expectedResult: boolean, + token?: CancellationToken + ): Promise<any> + execute( + command: 'reload', + args: Proto.ReloadRequestArgs, + expectedResult: boolean, + token?: CancellationToken + ): Promise<any> + execute( + command: 'compilerOptionsForInferredProjects', + args: Proto.SetCompilerOptionsForInferredProjectsArgs, + token?: CancellationToken + ): Promise<any> + execute( + command: 'navtree', + args: Proto.FileRequestArgs, + token?: CancellationToken + ): Promise<Proto.NavTreeResponse> + execute( + command: 'getCodeFixes', + args: Proto.CodeFixRequestArgs, + token?: CancellationToken + ): Promise<Proto.GetCodeFixesResponse> + execute( + command: 'getSupportedCodeFixes', + args: null, + token?: CancellationToken + ): Promise<Proto.GetSupportedCodeFixesResponse> + execute( + command: 'getCombinedCodeFix', + args: Proto.GetCombinedCodeFixRequestArgs, + token?: CancellationToken + ): Promise<Proto.GetCombinedCodeFixResponse> + execute( + command: 'docCommentTemplate', + args: Proto.FileLocationRequestArgs, + token?: CancellationToken + ): Promise<Proto.DocCommandTemplateResponse> + execute( + command: 'getApplicableRefactors', + args: Proto.GetApplicableRefactorsRequestArgs, + token?: CancellationToken + ): Promise<Proto.GetApplicableRefactorsResponse> + execute( + command: 'getEditsForRefactor', + args: Proto.GetEditsForRefactorRequestArgs, + token?: CancellationToken + ): Promise<Proto.GetEditsForRefactorResponse> + execute( + command: 'getEditsForFileRename', + args: Proto.GetEditsForFileRenameRequestArgs, + token?: CancellationToken + ): Promise<Proto.GetEditsForFileRenameResponse> + execute( + command: 'applyCodeActionCommand', + args: Proto.ApplyCodeActionCommandRequestArgs, + token?: CancellationToken + ): Promise<Proto.ApplyCodeActionCommandResponse> + execute( + command: 'organizeImports', + args: Proto.OrganizeImportsRequestArgs, + token?: CancellationToken + ): Promise<Proto.OrganizeImportsResponse> + execute( + command: 'getOutliningSpans', + args: Proto.FileRequestArgs, + token: CancellationToken + ): Promise<Proto.OutliningSpansResponse> + execute( + command: string, + args: any, + expectedResult: boolean | CancellationToken, + token?: CancellationToken + ): Promise<any> +} diff --git a/src/server/typescriptServiceClient.ts b/src/server/typescriptServiceClient.ts new file mode 100644 index 0000000..8717e7b --- /dev/null +++ b/src/server/typescriptServiceClient.ts @@ -0,0 +1,834 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import cp from 'child_process' +import fs from 'fs' +import os from 'os' +import path from 'path' +import { CancellationToken, Disposable, Emitter, Event } from 'vscode-languageserver-protocol' +import Uri from 'vscode-uri' +import which from 'which' +import { DiagnosticKind, ServiceStat, workspace, disposeAll } from 'coc.nvim' +import FileConfigurationManager from './features/fileConfigurationManager' +import * as Proto from './protocol' +import { ITypeScriptServiceClient } from './typescriptService' +import API from './utils/api' +import { TsServerLogLevel, TypeScriptServiceConfiguration } from './utils/configuration' +import Logger from './utils/logger' +import { fork, getTempFile, IForkOptions, makeRandomHexString } from './utils/process' +import Tracer from './utils/tracer' +import { inferredProjectConfig } from './utils/tsconfig' +import { TypeScriptVersion, TypeScriptVersionProvider } from './utils/versionProvider' +import { ICallback, Reader } from './utils/wireProtocol' + +interface CallbackItem { + c: (value: any) => void + e: (err: any) => void + start: number +} + +class CallbackMap { + private readonly callbacks: Map<number, CallbackItem> = new Map() + public pendingResponses = 0 + + public destroy(e: any): void { + for (const callback of this.callbacks.values()) { + callback.e(e) + } + this.callbacks.clear() + this.pendingResponses = 0 + } + + public add(seq: number, callback: CallbackItem): void { + this.callbacks.set(seq, callback) + ++this.pendingResponses + } + + public fetch(seq: number): CallbackItem | undefined { + const callback = this.callbacks.get(seq) + this.delete(seq) + return callback + } + + private delete(seq: number): void { + if (this.callbacks.delete(seq)) { + --this.pendingResponses + } + } +} + +interface RequestItem { + request: Proto.Request + callbacks: CallbackItem | null +} + +class RequestQueue { + private queue: RequestItem[] = [] + private sequenceNumber = 0 + + public get length(): number { + return this.queue.length + } + + public push(item: RequestItem): void { + this.queue.push(item) + } + + public shift(): RequestItem | undefined { + return this.queue.shift() + } + + public tryCancelPendingRequest(seq: number): boolean { + for (let i = 0; i < this.queue.length; i++) { + if (this.queue[i].request.seq === seq) { + this.queue.splice(i, 1) + return true + } + } + return false + } + + public createRequest(command: string, args: any): Proto.Request { + return { + seq: this.sequenceNumber++, + type: 'request', + command, + arguments: args + } + } +} + +class ForkedTsServerProcess { + constructor(private childProcess: cp.ChildProcess) { } + + public onError(cb: (err: Error) => void): void { + this.childProcess.on('error', cb) + } + + public onExit(cb: (err: any) => void): void { + this.childProcess.on('exit', cb) + } + + public write(serverRequest: Proto.Request): void { + this.childProcess.stdin.write( + JSON.stringify(serverRequest) + '\r\n', + 'utf8' + ) + } + + public createReader( + callback: ICallback<Proto.Response>, + onError: (error: any) => void + ): void { + // tslint:disable-next-line:no-unused-expression + new Reader<Proto.Response>(this.childProcess.stdout, callback, onError) + } + + public kill(): void { + this.childProcess.kill() + } +} + +export interface TsDiagnostics { + readonly kind: DiagnosticKind + readonly resource: Uri + readonly diagnostics: Proto.Diagnostic[] +} + +export default class TypeScriptServiceClient implements ITypeScriptServiceClient { + public state = ServiceStat.Initial + public readonly logger: Logger = new Logger() + private fileConfigurationManager: FileConfigurationManager + private pathSeparator: string + private tracer: Tracer + private _configuration: TypeScriptServiceConfiguration + private versionProvider: TypeScriptVersionProvider + private tsServerLogFile: string | null = null + private servicePromise: Thenable<ForkedTsServerProcess> | null + private lastError: Error | null + private lastStart: number + private numberRestarts: number + private cancellationPipeName: string | null = null + private requestQueue: RequestQueue + private callbacks: CallbackMap + private readonly _onTsServerStarted = new Emitter<API>() + private readonly _onProjectLanguageServiceStateChanged = new Emitter<Proto.ProjectLanguageServiceStateEventBody>() + private readonly _onDidBeginInstallTypings = new Emitter<Proto.BeginInstallTypesEventBody>() + private readonly _onDidEndInstallTypings = new Emitter<Proto.EndInstallTypesEventBody>() + private readonly _onTypesInstallerInitializationFailed = new Emitter< + Proto.TypesInstallerInitializationFailedEventBody + >() + private _apiVersion: API + private readonly disposables: Disposable[] = [] + + constructor() { + this.pathSeparator = path.sep + this.lastStart = Date.now() + this.servicePromise = null + this.lastError = null + this.numberRestarts = 0 + this.fileConfigurationManager = new FileConfigurationManager(this) + this.requestQueue = new RequestQueue() + this.callbacks = new CallbackMap() + this._configuration = TypeScriptServiceConfiguration.loadFromWorkspace() + this.versionProvider = new TypeScriptVersionProvider(this._configuration) + this._apiVersion = API.defaultVersion + this.tracer = new Tracer(this.logger) + const onInstalled = name => { + if (name == 'typescript') { + this.restartTsServer().catch(e => { + this.logger.error(e.stack) + }) + workspace.terminal.removeListener('installed', onInstalled) + } + } + workspace.terminal.on('installed', onInstalled) + this.disposables.push(Disposable.create(() => { + workspace.terminal.removeListener('installed', onInstalled) + })) + } + + private _onDiagnosticsReceived = new Emitter<TsDiagnostics>() + public get onDiagnosticsReceived(): Event<TsDiagnostics> { + return this._onDiagnosticsReceived.event + } + + private _onConfigDiagnosticsReceived = new Emitter<Proto.ConfigFileDiagnosticEvent>() + public get onConfigDiagnosticsReceived(): Event<Proto.ConfigFileDiagnosticEvent> { + return this._onConfigDiagnosticsReceived.event + } + + private _onResendModelsRequested = new Emitter<void>() + public get onResendModelsRequested(): Event<void> { + return this._onResendModelsRequested.event + } + + public get configuration(): TypeScriptServiceConfiguration { + return this._configuration + } + + public dispose(): void { + if (this.servicePromise) { + this.servicePromise + .then(childProcess => { + childProcess.kill() + }) + .then(undefined, () => void 0) + } + + disposeAll(this.disposables) + this.logger.dispose() + this._onTsServerStarted.dispose() + this._onResendModelsRequested.dispose() + } + + private info(message: string, data?: any): void { + this.logger.info(message, data) + } + + private error(message: string, data?: any): void { + this.logger.error(message, data) + } + + public restartTsServer(): Promise<any> { + const start = () => { + this.servicePromise = this.startService(true) + return this.servicePromise + } + + if (this.servicePromise) { + return Promise.resolve(this.servicePromise.then(childProcess => { + this.state = ServiceStat.Stopping + this.info('Killing TS Server') + childProcess.kill() + this.servicePromise = null + }).then(start)) + } else { + return Promise.resolve(start()) + } + } + + public stop(): Promise<void> { + if (!this.servicePromise) return + return new Promise((resolve, reject) => { + this.servicePromise.then(childProcess => { + if (this.state == ServiceStat.Running) { + this.info('Killing TS Server') + childProcess.onExit(() => { + resolve() + }) + childProcess.kill() + this.servicePromise = null + } else { + resolve() + } + }, reject) + }) + } + + public get onTsServerStarted(): Event<API> { + return this._onTsServerStarted.event + } + + public get onProjectLanguageServiceStateChanged(): Event< + Proto.ProjectLanguageServiceStateEventBody + > { + return this._onProjectLanguageServiceStateChanged.event + } + + public get onDidBeginInstallTypings(): Event<Proto.BeginInstallTypesEventBody> { + return this._onDidBeginInstallTypings.event + } + + public get onDidEndInstallTypings(): Event<Proto.EndInstallTypesEventBody> { + return this._onDidEndInstallTypings.event + } + + public get onTypesInstallerInitializationFailed(): Event<Proto.TypesInstallerInitializationFailedEventBody> { + return this._onTypesInstallerInitializationFailed.event + } + + public get apiVersion(): API { + return this._apiVersion + } + + private service(): Thenable<ForkedTsServerProcess> { + if (this.servicePromise) { + return this.servicePromise + } + if (this.lastError) { + return Promise.reject<ForkedTsServerProcess>(this.lastError) + } + return this.startService().then(() => { + if (this.servicePromise) { + return this.servicePromise + } + }) + } + + public ensureServiceStarted(): void { + if (!this.servicePromise) { + this.startService().catch(err => { + workspace.showMessage(`TSServer start failed: ${err.message}`, 'error') + this.error(`Service start failed: ${err.stack}`) + }) + } + } + + private async startService(resendModels = false): Promise<ForkedTsServerProcess> { + let currentVersion = this.versionProvider.getLocalVersion(workspace.root) + if (!currentVersion || !fs.existsSync(currentVersion.tsServerPath)) { + currentVersion = await this.versionProvider.getDefaultVersion() + } + if (!currentVersion || !currentVersion.isValid) { + workspace.showMessage('Can not find tsserver, try installing...', 'error') + await workspace.terminal.installModule('typescript', 'tsserver') + return + } + workspace.showMessage(`Using tsserver from: ${currentVersion.path}`) // tslint:disable-line + this._apiVersion = currentVersion.version + this.requestQueue = new RequestQueue() + this.callbacks = new CallbackMap() + this.lastError = null + const tsServerForkArgs = await this.getTsServerArgs() + const debugPort = this._configuration.debugPort + const options = { + execArgv: debugPort ? [`--inspect=${debugPort}`] : [], // [`--debug-brk=5859`] + cwd: workspace.root + } + this.servicePromise = this.startProcess(currentVersion, tsServerForkArgs, options, resendModels) + return this.servicePromise + } + + private startProcess(currentVersion: TypeScriptVersion, args: string[], options: IForkOptions, resendModels: boolean): Promise<ForkedTsServerProcess> { + this.state = ServiceStat.Starting + return new Promise((resolve, reject) => { + try { + fork( + currentVersion.tsServerPath, + args, + options, + this.logger, + (err: any, childProcess: cp.ChildProcess | null) => { + if (err || !childProcess) { + this.state = ServiceStat.StartFailed + this.lastError = err + this.error('Starting TSServer failed with error.', err.stack) + return + } + this.state = ServiceStat.Running + this.info('Started TSServer', JSON.stringify(currentVersion, null, 2)) + const handle = new ForkedTsServerProcess(childProcess) + this.lastStart = Date.now() + + handle.onError((err: Error) => { + this.lastError = err + this.error('TSServer errored with error.', err) + this.error(`TSServer log file: ${this.tsServerLogFile || ''}`) + workspace.showMessage(`TSServer errored with error. ${err.message}`, 'error') + this.serviceExited(false) + }) + handle.onExit((code: any) => { + if (code == null) { + this.info('TSServer normal exit') + } else { + this.error(`TSServer exited with code: ${code}`) + } + this.info(`TSServer log file: ${this.tsServerLogFile || ''}`) + this.serviceExited(code != null) + }) + + handle.createReader( + msg => { + this.dispatchMessage(msg) + }, + error => { + this.error('ReaderError', error) + } + ) + resolve(handle) + this._onTsServerStarted.fire(currentVersion.version) + this.serviceStarted(resendModels) + } + ) + } catch (e) { + reject(e) + } + }) + } + + public async openTsServerLogFile(): Promise<boolean> { + const isRoot = process.getuid && process.getuid() == 0 + let echoErr = (msg: string) => { + workspace.showMessage(msg, 'error') + } + if (isRoot) { + echoErr('Log disabled for root user.') + return false + } + if (!this.apiVersion.gte(API.v222)) { + echoErr('TS Server logging requires TS 2.2.2+') + return false + } + if (this._configuration.tsServerLogLevel === TsServerLogLevel.Off) { + echoErr(`TS Server logging is off. Change 'tsserver.log' in 'coc-settings.json' to enable`) + return false + } + if (!this.tsServerLogFile) { + echoErr('TS Server has not started logging.') + return false + } + try { + await workspace.nvim.command(`edit ${this.tsServerLogFile}`) + return true + } catch { + echoErr('Could not open TS Server log file') + return false + } + } + + private serviceStarted(resendModels: boolean): void { + let document = workspace.getDocument(workspace.bufnr) + if (document) { + this.fileConfigurationManager.ensureConfigurationForDocument(document.textDocument) // tslint:disable-line + } else { + const configureOptions: Proto.ConfigureRequestArguments = { + hostInfo: 'nvim-coc' + } + this.execute('configure', configureOptions) // tslint:disable-line + } + this.setCompilerOptionsForInferredProjects(this._configuration) + if (resendModels) { + this._onResendModelsRequested.fire(void 0) + } + } + + private setCompilerOptionsForInferredProjects( + configuration: TypeScriptServiceConfiguration + ): void { + if (!this.apiVersion.gte(API.v206)) return + const args: Proto.SetCompilerOptionsForInferredProjectsArgs = { + options: this.getCompilerOptionsForInferredProjects(configuration) + } + this.execute('compilerOptionsForInferredProjects', args, true) // tslint:disable-line + } + + private getCompilerOptionsForInferredProjects( + configuration: TypeScriptServiceConfiguration + ): Proto.ExternalProjectCompilerOptions { + return { + ...inferredProjectConfig(configuration), + allowJs: true, + allowSyntheticDefaultImports: true, + allowNonTsExtensions: true + } + } + + private serviceExited(restart: boolean): void { + this.state = ServiceStat.Stopped + this.servicePromise = null + this.tsServerLogFile = null + this.callbacks.destroy(new Error('Service died.')) + this.callbacks = new CallbackMap() + if (restart) { + const diff = Date.now() - this.lastStart + this.numberRestarts++ + let startService = true + if (this.numberRestarts > 5) { + this.numberRestarts = 0 + if (diff < 10 * 1000 /* 10 seconds */) { + this.lastStart = Date.now() + startService = false + workspace.showMessage('The TypeScript language service died 5 times right after it got started.', 'error') // tslint:disable-line + } else if (diff < 60 * 1000 /* 1 Minutes */) { + this.lastStart = Date.now() + workspace.showMessage('The TypeScript language service died unexpectedly 5 times in the last 5 Minutes.', 'error') // tslint:disable-line + } + } + if (startService) { + this.startService(true) // tslint:disable-line + } + } + } + + public toPath(uri: string): string { + return this.normalizePath(Uri.parse(uri)) + } + + public toResource(filepath: string): string { + if (this._apiVersion.gte(API.v213)) { + if (filepath.startsWith('untitled:')) { + let resource = Uri.parse(filepath) + if (this.inMemoryResourcePrefix) { + const dirName = path.dirname(resource.path) + const fileName = path.basename(resource.path) + if (fileName.startsWith(this.inMemoryResourcePrefix)) { + resource = resource.with({ path: path.posix.join(dirName, fileName.slice(this.inMemoryResourcePrefix.length)) }) + } + } + return resource.toString() + } + } + return Uri.file(filepath).toString() + } + + public normalizePath(resource: Uri): string | null { + if (this._apiVersion.gte(API.v213)) { + if (resource.scheme !== 'file') { + const dirName = path.dirname(resource.path) + const fileName = this.inMemoryResourcePrefix + path.basename(resource.path) + return resource + .with({ path: path.posix.join(dirName, fileName) }) + .toString(true) + } + } + + const result = resource.fsPath + if (!result) return null + + // Both \ and / must be escaped in regular expressions + return result.replace(new RegExp('\\' + this.pathSeparator, 'g'), '/') + } + + private get inMemoryResourcePrefix(): string { + return this._apiVersion.gte(API.v270) ? '^' : '' + } + + public asUrl(filepath: string): Uri { + if (this._apiVersion.gte(API.v213)) { + if (filepath.startsWith('untitled:')) { + let resource = Uri.parse(filepath) + if (this.inMemoryResourcePrefix) { + const dirName = path.dirname(resource.path) + const fileName = path.basename(resource.path) + if (fileName.startsWith(this.inMemoryResourcePrefix)) { + resource = resource.with({ + path: path.posix.join( + dirName, + fileName.slice(this.inMemoryResourcePrefix.length) + ) + }) + } + } + return resource + } + } + return Uri.file(filepath) + } + + public execute( + command: string, + args: any, + expectsResultOrToken?: boolean | CancellationToken + ): Promise<any> { + if (this.servicePromise == null) { + return Promise.resolve() + } + let token: CancellationToken | undefined + let expectsResult = true + if (typeof expectsResultOrToken === 'boolean') { + expectsResult = expectsResultOrToken + } else { + token = expectsResultOrToken + } + + const request = this.requestQueue.createRequest(command, args) + const requestInfo: RequestItem = { + request, + callbacks: null + } + let result: Promise<any> + if (expectsResult) { + let wasCancelled = false + result = new Promise<any>((resolve, reject) => { + requestInfo.callbacks = { c: resolve, e: reject, start: Date.now() } + if (token) { + token.onCancellationRequested(() => { + wasCancelled = true + this.tryCancelRequest(request.seq) + }) + } + }).catch((err: any) => { + if (!wasCancelled && command != 'signatureHelp') { + this.error(`'${command}' request failed with error.`, err) + } + throw err + }) + } else { + result = Promise.resolve(null) + } + this.requestQueue.push(requestInfo) + this.sendNextRequests() + + return result + } + + private sendNextRequests(): void { + while ( + this.callbacks.pendingResponses === 0 && + this.requestQueue.length > 0 + ) { + const item = this.requestQueue.shift() + if (item) { + this.sendRequest(item) + } + } + } + + private sendRequest(requestItem: RequestItem): void { + const serverRequest = requestItem.request + this.tracer.traceRequest( + serverRequest, + !!requestItem.callbacks, + this.requestQueue.length + ) + if (requestItem.callbacks) { + this.callbacks.add(serverRequest.seq, requestItem.callbacks) + } + this.service() + .then(childProcess => { + childProcess.write(serverRequest) + }) + .then(undefined, err => { + const callback = this.callbacks.fetch(serverRequest.seq) + if (callback) { + callback.e(err) + } + }) + } + + private tryCancelRequest(seq: number): boolean { + try { + if (this.requestQueue.tryCancelPendingRequest(seq)) { + this.tracer.logTrace(`TypeScript Service: canceled request with sequence number ${seq}`) + return true + } + + if (this.apiVersion.gte(API.v222) && this.cancellationPipeName) { + this.tracer.logTrace(`TypeScript Service: trying to cancel ongoing request with sequence number ${seq}`) + try { + fs.writeFileSync(this.cancellationPipeName + seq, '') + } catch { + // noop + } + return true + } + + this.tracer.logTrace( + `TypeScript Service: tried to cancel request with sequence number ${seq}. But request got already delivered.` + ) + return false + } finally { + const p = this.callbacks.fetch(seq) + if (p) { + p.e(new Error(`Cancelled Request ${seq}`)) + } + } + } + + private dispatchMessage(message: Proto.Message): void { + try { + if (message.type === 'response') { + const response: Proto.Response = message as Proto.Response + const p = this.callbacks.fetch(response.request_seq) + if (p) { + this.tracer.traceResponse(response, p.start) + if (response.success) { + p.c(response) + } else { + p.e(response) + } + } + } else if (message.type === 'event') { + const event: Proto.Event = message as Proto.Event + this.tracer.traceEvent(event) + this.dispatchEvent(event) + } else { + throw new Error('Unknown message type ' + message.type + ' received') + } + } finally { + this.sendNextRequests() + } + } + + private dispatchEvent(event: Proto.Event): void { + switch (event.event) { + case 'syntaxDiag': + case 'semanticDiag': + case 'suggestionDiag': + const diagnosticEvent: Proto.DiagnosticEvent = event + if (diagnosticEvent.body && diagnosticEvent.body.diagnostics) { + this._onDiagnosticsReceived.fire({ + kind: getDignosticsKind(event), + resource: this.asUrl(diagnosticEvent.body.file), + diagnostics: diagnosticEvent.body.diagnostics + }) + } + break + + case 'configFileDiag': + this._onConfigDiagnosticsReceived.fire( + event as Proto.ConfigFileDiagnosticEvent + ) + break + + case 'projectLanguageServiceState': + if (event.body) { + this._onProjectLanguageServiceStateChanged.fire( + (event as Proto.ProjectLanguageServiceStateEvent).body + ) + } + break + + case 'beginInstallTypes': + if (event.body) { + this._onDidBeginInstallTypings.fire( + (event as Proto.BeginInstallTypesEvent).body + ) + } + break + + case 'endInstallTypes': + if (event.body) { + this._onDidEndInstallTypings.fire( + (event as Proto.EndInstallTypesEvent).body + ) + } + break + + case 'typesInstallerInitializationFailed': + if (event.body) { + this._onTypesInstallerInitializationFailed.fire( + (event as Proto.TypesInstallerInitializationFailedEvent).body + ) + } + break + } + } + + private async getTsServerArgs(): Promise<string[]> { + const args: string[] = [] + args.push('--allowLocalPluginLoads') + + if (this.apiVersion.gte(API.v250)) { + args.push('--useInferredProjectPerProjectRoot') + } else { + args.push('--useSingleInferredProject') + } + + if (this.apiVersion.gte(API.v206) && this._configuration.disableAutomaticTypeAcquisition) { + args.push('--disableAutomaticTypingAcquisition') + } + + if (this.apiVersion.gte(API.v222)) { + this.cancellationPipeName = getTempFile(`tscancellation-${makeRandomHexString(20)}`) + args.push('--cancellationPipeName', this.cancellationPipeName + '*') + } + + if (this.apiVersion.gte(API.v222)) { + const isRoot = process.getuid && process.getuid() == 0 + if (this._configuration.tsServerLogLevel !== TsServerLogLevel.Off && !isRoot) { + const logDir = os.tmpdir() + if (logDir) { + this.tsServerLogFile = path.join(logDir, `coc-nvim-tsc.log`) + this.info('TSServer log file :', this.tsServerLogFile) + } else { + this.tsServerLogFile = null + this.error('Could not create TSServer log directory') + } + + if (this.tsServerLogFile) { + args.push( + '--logVerbosity', + TsServerLogLevel.toString(this._configuration.tsServerLogLevel) + ) + args.push('--logFile', this.tsServerLogFile) + } + } + } + + if (this.apiVersion.gte(API.v230)) { + const plugins = this._configuration.tsServerPluginNames + const pluginRoot = this._configuration.tsServerPluginRoot + if (plugins.length) { + args.push('--globalPlugins', plugins.join(',')) + if (pluginRoot) { + args.push('--pluginProbeLocations', pluginRoot) + } + } + } + + if (this._configuration.typingsCacheLocation) { + args.push('--globalTypingsCacheLocation', `"${this._configuration.typingsCacheLocation}"`) + } + + if (this.apiVersion.gte(API.v234)) { + if (this._configuration.npmLocation) { + args.push('--npmLocation', `"${this._configuration.npmLocation}"`) + } else { + try { + args.push('--npmLocation', `"${which.sync('npm')}"`) + } catch (e) { } // tslint:disable-line + } + } + + if (this.apiVersion.gte(API.v291)) { + args.push('--noGetErrOnBackgroundUpdate') + } + + return args + } +} + +function getDignosticsKind(event: Proto.Event): DiagnosticKind { + switch (event.event) { + case 'syntaxDiag': + return DiagnosticKind.Syntax + case 'semanticDiag': + return DiagnosticKind.Semantic + case 'suggestionDiag': + return DiagnosticKind.Suggestion + } + throw new Error('Unknown dignostics kind') +} diff --git a/src/server/typescriptServiceClientHost.ts b/src/server/typescriptServiceClientHost.ts new file mode 100644 index 0000000..775bced --- /dev/null +++ b/src/server/typescriptServiceClientHost.ts @@ -0,0 +1,201 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { DiagnosticKind, disposeAll, workspace } from 'coc.nvim' +import { Diagnostic, DiagnosticSeverity, Disposable } from 'vscode-languageserver-protocol' +import Uri from 'vscode-uri' +import LanguageProvider from './languageProvider' +import * as Proto from './protocol' +import * as PConst from './protocol.const' +import TypeScriptServiceClient from './typescriptServiceClient' +import { LanguageDescription } from './utils/languageDescription' +import * as typeConverters from './utils/typeConverters' +import TypingsStatus, { AtaProgressReporter } from './utils/typingsStatus' + +// Style check diagnostics that can be reported as warnings +const styleCheckDiagnostics = [ + 6133, // variable is declared but never used + 6138, // property is declared but its value is never read + 7027, // unreachable code detected + 7028, // unused label + 7029, // fall through case in switch + 7030 // not all code paths return a value +] + +export default class TypeScriptServiceClientHost implements Disposable { + private readonly ataProgressReporter: AtaProgressReporter + private readonly typingsStatus: TypingsStatus + private readonly client: TypeScriptServiceClient + private readonly languages: LanguageProvider[] = [] + private readonly languagePerId = new Map<string, LanguageProvider>() + private readonly disposables: Disposable[] = [] + private reportStyleCheckAsWarnings = true + + constructor(descriptions: LanguageDescription[]) { + const handleProjectChange = () => { + setTimeout(() => { + this.triggerAllDiagnostics() + }, 1500) + } + + const configFileWatcher = workspace.createFileSystemWatcher('**/[tj]sconfig.json') + this.disposables.push(configFileWatcher) + configFileWatcher.onDidCreate( + this.reloadProjects, + this, + this.disposables + ) + configFileWatcher.onDidDelete( + this.reloadProjects, + this, + this.disposables + ) + configFileWatcher.onDidChange(handleProjectChange, this, this.disposables) + + this.client = new TypeScriptServiceClient() + this.disposables.push(this.client) + this.client.onDiagnosticsReceived(({ kind, resource, diagnostics }) => { + this.diagnosticsReceived(kind, resource, diagnostics) + }, null, this.disposables) + + this.client.onConfigDiagnosticsReceived(diag => { + let { body } = diag + if (body) { + let { configFile, diagnostics } = body + if (diagnostics.length) { + workspace.showMessage(`Invalid config file: ${configFile}`, 'error') + } + } + }, null, this.disposables) + + this.typingsStatus = new TypingsStatus(this.client) + this.ataProgressReporter = new AtaProgressReporter(this.client) + for (const description of descriptions) { // tslint:disable-line + const manager = new LanguageProvider( + this.client, + description, + this.typingsStatus + ) + this.languages.push(manager) + this.disposables.push(manager) + this.languagePerId.set(description.id, manager) + } + + this.client.ensureServiceStarted() + this.client.onTsServerStarted(() => { + this.triggerAllDiagnostics() + }) + this.configurationChanged() + } + + public dispose(): void { + disposeAll(this.disposables) + this.typingsStatus.dispose() + this.ataProgressReporter.dispose() + } + + public reset(): void { + for (let lang of this.languages) { + lang.fileConfigurationManager.reset() + } + } + + public get serviceClient(): TypeScriptServiceClient { + return this.client + } + + public reloadProjects(): void { + this.client.execute('reloadProjects', null, false) // tslint:disable-line + this.triggerAllDiagnostics() + } + + // typescript or javascript + public getProvider(languageId: string): LanguageProvider { + return this.languagePerId.get(languageId) + } + + private configurationChanged(): void { + const config = workspace.getConfiguration('tsserver') + this.reportStyleCheckAsWarnings = config.get('reportStyleChecksAsWarnings', true) + } + + public findLanguage(resource: Uri): LanguageProvider | null { + try { + return this.languages.find(language => language.handles(resource)) + } catch { + return null + } + } + + public handles(uri: string): boolean { + return this.findLanguage(Uri.parse(uri)) != null + } + + private triggerAllDiagnostics(): void { + for (const language of this.languagePerId.values()) { + language.triggerAllDiagnostics() + } + } + + private diagnosticsReceived( + kind: DiagnosticKind, + resource: Uri, + diagnostics: Proto.Diagnostic[] + ): void { + const language = this.findLanguage(resource) + if (language) { + language.diagnosticsReceived( + kind, + resource, + this.createMarkerDatas(diagnostics)) + } + } + + private createMarkerDatas(diagnostics: Proto.Diagnostic[]): Diagnostic[] { + return diagnostics.map(tsDiag => this.tsDiagnosticToLspDiagnostic(tsDiag)) + } + + private tsDiagnosticToLspDiagnostic(diagnostic: Proto.Diagnostic): Diagnostic { + const { start, end, text } = diagnostic + const range = { + start: typeConverters.Position.fromLocation(start), + end: typeConverters.Position.fromLocation(end) + } + return { + range, + message: text, + code: diagnostic.code ? diagnostic.code : null, + severity: this.getDiagnosticSeverity(diagnostic), + source: diagnostic.source || 'tsserver', + } + } + + private getDiagnosticSeverity(diagnostic: Proto.Diagnostic): DiagnosticSeverity { + if ( + this.reportStyleCheckAsWarnings && + this.isStyleCheckDiagnostic(diagnostic.code) && + diagnostic.category === PConst.DiagnosticCategory.error + ) { + return DiagnosticSeverity.Warning + } + + switch (diagnostic.category) { + case PConst.DiagnosticCategory.error: + return DiagnosticSeverity.Error + + case PConst.DiagnosticCategory.warning: + return DiagnosticSeverity.Warning + + case PConst.DiagnosticCategory.suggestion: + return DiagnosticSeverity.Information + + default: + return DiagnosticSeverity.Error + } + } + + private isStyleCheckDiagnostic(code: number | undefined): boolean { + return code ? styleCheckDiagnostics.indexOf(code) !== -1 : false + } +} diff --git a/src/server/utils/api.ts b/src/server/utils/api.ts new file mode 100644 index 0000000..a5746ee --- /dev/null +++ b/src/server/utils/api.ts @@ -0,0 +1,53 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as semver from 'semver' + +export default class API { + private static fromSimpleString(value: string): API { + return new API(value, value) + } + + public static readonly defaultVersion = API.fromSimpleString('1.0.0') + public static readonly v203 = API.fromSimpleString('2.0.3') + public static readonly v206 = API.fromSimpleString('2.0.6') + public static readonly v208 = API.fromSimpleString('2.0.8') + public static readonly v213 = API.fromSimpleString('2.1.3') + public static readonly v220 = API.fromSimpleString('2.2.0') + public static readonly v222 = API.fromSimpleString('2.2.2') + public static readonly v230 = API.fromSimpleString('2.3.0') + public static readonly v234 = API.fromSimpleString('2.3.4') + public static readonly v240 = API.fromSimpleString('2.4.0') + public static readonly v250 = API.fromSimpleString('2.5.0') + public static readonly v260 = API.fromSimpleString('2.6.0') + public static readonly v270 = API.fromSimpleString('2.7.0') + public static readonly v280 = API.fromSimpleString('2.8.0') + public static readonly v290 = API.fromSimpleString('2.9.0') + public static readonly v291 = API.fromSimpleString('2.9.1') + public static readonly v292 = API.fromSimpleString('2.9.2') + public static readonly v300 = API.fromSimpleString('3.0.0') + + public static fromVersionString(versionString: string): API { + let version = semver.valid(versionString) + if (!version) { + return new API('invalid version', '1.0.0') + } + + // Cut off any prerelease tag since we sometimes consume those on purpose. + const index = versionString.indexOf('-') + if (index >= 0) { + version = version.substr(0, index) + } + return new API(versionString, version) + } + + private constructor( + public readonly versionString: string, + private readonly version: string + ) { } + + public gte(other: API): boolean { + return semver.gte(this.version, other.version) + } +} diff --git a/src/server/utils/async.ts b/src/server/utils/async.ts new file mode 100644 index 0000000..9b7f6f5 --- /dev/null +++ b/src/server/utils/async.ts @@ -0,0 +1,61 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +export type ITask<T> = () => T + +export class Delayer<T> { + public defaultDelay: number + private timeout: any // Timer + private completionPromise: Promise<T | null> | null + private onSuccess: ((value?: T | Thenable<T>) => void) | null + private task: ITask<T> | null + + constructor(defaultDelay: number) { + this.defaultDelay = defaultDelay + this.timeout = null + this.completionPromise = null + this.onSuccess = null + this.task = null + } + + public trigger( + task: ITask<T>, + delay: number = this.defaultDelay + ): Promise<T | null> { + this.task = task + if (delay >= 0) { + this.cancelTimeout() + } + + if (!this.completionPromise) { + this.completionPromise = new Promise<T>(resolve => { + this.onSuccess = resolve + }).then(() => { + this.completionPromise = null + this.onSuccess = null + let result = this.task && this.task() + this.task = null + return result + }) + } + + if (delay >= 0 || this.timeout === null) { + this.timeout = setTimeout(() => { + this.timeout = null + if (this.onSuccess) { + this.onSuccess(undefined) + } + }, delay >= 0 ? delay : this.defaultDelay) + } + + return this.completionPromise + } + + private cancelTimeout(): void { + if (this.timeout !== null) { + clearTimeout(this.timeout) + this.timeout = null + } + } +} diff --git a/src/server/utils/codeAction.ts b/src/server/utils/codeAction.ts new file mode 100644 index 0000000..e0d3c42 --- /dev/null +++ b/src/server/utils/codeAction.ts @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { WorkspaceEdit } from 'vscode-languageserver-protocol' +import { workspace } from 'coc.nvim' +import * as Proto from '../protocol' +import { ITypeScriptServiceClient } from '../typescriptService' +import * as typeConverters from './typeConverters' + +export function getEditForCodeAction( + client: ITypeScriptServiceClient, + action: Proto.CodeAction +): WorkspaceEdit | undefined { + return action.changes && action.changes.length + ? typeConverters.WorkspaceEdit.fromFileCodeEdits(client, action.changes) + : undefined +} + +export async function applyCodeAction( + client: ITypeScriptServiceClient, + action: Proto.CodeAction +): Promise<boolean> { + const workspaceEdit = getEditForCodeAction(client, action) + if (workspaceEdit) { + if (!(await workspace.applyEdit(workspaceEdit))) { + return false + } + } + return applyCodeActionCommands(client, action) +} + +export async function applyCodeActionCommands( + client: ITypeScriptServiceClient, + action: Proto.CodeAction +): Promise<boolean> { + // make sure there is command + if (action.commands && action.commands.length) { + for (const command of action.commands) { + const response = await client.execute('applyCodeActionCommand', { command }) + if (!response || !response.body) { + return false + } + } + } + return true +} diff --git a/src/server/utils/completionItem.ts b/src/server/utils/completionItem.ts new file mode 100644 index 0000000..ad923b5 --- /dev/null +++ b/src/server/utils/completionItem.ts @@ -0,0 +1,149 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { CompletionItem, CompletionItemKind, InsertTextFormat, Position, TextEdit } from 'vscode-languageserver-protocol' +import { Document, workspace } from 'coc.nvim' +import * as Proto from '../protocol' +import * as PConst from '../protocol.const' + +export function resolveItem( + item: CompletionItem, + document: Document, +): void { + let { textEdit, label } = item // tslint:disable-line + let { position } = item.data + if (textEdit) return + // try replace more characters after cursor + const wordRange = document.getWordRangeAtPosition(position) + let text = document.textDocument.getText({ + start: { + line: position.line, + character: Math.max(0, position.character - label.length), + }, + end: { + line: position.line, + character: position.character + } + }) + + text = text.toLowerCase() + const entryName = label.toLowerCase() + + for (let i = entryName.length; i >= 0; --i) { + if (text.endsWith(entryName.substr(0, i)) && + (!wordRange || + wordRange.start.character > position.character - i)) { + item.textEdit = { + newText: label, + range: { + start: { + line: position.line, + character: Math.max(0, position.character - i) + }, + end: { + line: position.line, + character: position.character + } + } + } + break + } + } +} + +export function convertCompletionEntry( + tsEntry: Proto.CompletionEntry, + uri: string, + position: Position, + useCodeSnippetsOnMethodSuggest: boolean +): CompletionItem { + let label = tsEntry.name + let sortText = tsEntry.sortText + 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 + sortText = '\uffff' + sortText + } else { + sortText = tsEntry.sortText + } + let kind = convertKind(tsEntry.kind) + let insertTextFormat = ( + useCodeSnippetsOnMethodSuggest && + (kind === CompletionItemKind.Function || + kind === CompletionItemKind.Method) + ) ? InsertTextFormat.Snippet : InsertTextFormat.PlainText + + let textEdit: TextEdit = null + let insertText = tsEntry.insertText + if (insertText) { + let document = workspace.getDocument(uri) + textEdit = { + range: document.getWordRangeAtPosition(position), + newText: insertText + } + insertText = null + } + let optional = tsEntry.kindModifiers && tsEntry.kindModifiers.match(/\boptional\b/) + return { + label, + insertText, + kind, + textEdit, + insertTextFormat, + sortText, + data: { + uri, + optional, + position, + source: tsEntry.source || '' + } + } +} + +function convertKind(kind: string): CompletionItemKind { + switch (kind) { + case PConst.Kind.primitiveType: + case PConst.Kind.keyword: + return CompletionItemKind.Keyword + case PConst.Kind.const: + return CompletionItemKind.Constant + case PConst.Kind.let: + case PConst.Kind.variable: + case PConst.Kind.localVariable: + case PConst.Kind.alias: + return CompletionItemKind.Variable + case PConst.Kind.memberVariable: + case PConst.Kind.memberGetAccessor: + case PConst.Kind.memberSetAccessor: + return CompletionItemKind.Field + case PConst.Kind.function: + return CompletionItemKind.Function + case PConst.Kind.memberFunction: + case PConst.Kind.constructSignature: + case PConst.Kind.callSignature: + case PConst.Kind.indexSignature: + return CompletionItemKind.Method + case PConst.Kind.enum: + return CompletionItemKind.Enum + case PConst.Kind.module: + case PConst.Kind.externalModuleName: + return CompletionItemKind.Module + case PConst.Kind.class: + case PConst.Kind.type: + return CompletionItemKind.Class + case PConst.Kind.interface: + return CompletionItemKind.Interface + case PConst.Kind.warning: + case PConst.Kind.file: + case PConst.Kind.script: + return CompletionItemKind.File + case PConst.Kind.directory: + return CompletionItemKind.Folder + } + return CompletionItemKind.Property +} diff --git a/src/server/utils/configuration.ts b/src/server/utils/configuration.ts new file mode 100644 index 0000000..a26c643 --- /dev/null +++ b/src/server/utils/configuration.ts @@ -0,0 +1,109 @@ +import { workspace, WorkspaceConfiguration } from 'coc.nvim' +import which from 'which' + +export enum TsServerLogLevel { + Off, + Normal, + Terse, + Verbose +} + +export namespace TsServerLogLevel { + export function fromString(value: string): TsServerLogLevel { + switch (value && value.toLowerCase()) { + case 'normal': + return TsServerLogLevel.Normal + case 'terse': + return TsServerLogLevel.Terse + case 'verbose': + return TsServerLogLevel.Verbose + case 'off': + default: + return TsServerLogLevel.Off + } + } + + export function toString(value: TsServerLogLevel): string { + switch (value) { + case TsServerLogLevel.Normal: + return 'normal' + case TsServerLogLevel.Terse: + return 'terse' + case TsServerLogLevel.Verbose: + return 'verbose' + case TsServerLogLevel.Off: + default: + return 'off' + } + } +} + +export class TypeScriptServiceConfiguration { + private _configuration: WorkspaceConfiguration + private constructor() { + this._configuration = workspace.getConfiguration('tsserver') + + workspace.onDidChangeConfiguration(() => { + this._configuration = workspace.getConfiguration('tsserver') + }) + } + + public get locale(): string | null { + return this._configuration.get<string | null>('locale', null) + } + + public get globalTsdk(): string | null { + return this._configuration.get<string | null>('tsdk', null) + } + + public get tsServerLogLevel(): TsServerLogLevel { + return TsServerLogLevel.fromString(this._configuration.get<string | null>('log', null)) + } + + public get typingsCacheLocation(): string { + return this._configuration.get<string>('typingsCacheLocation', '') + } + + public get tsServerPluginNames(): string[] { + return this._configuration.get<string[]>('pluginNames', []) + } + + public get tsServerPluginRoot(): string | null { + return this._configuration.get<string | null>('tsServerPluginRoot', null) + } + + public get checkJs(): boolean { + return this._configuration.get<boolean>('implicitProjectConfig.checkJs', false) + } + + public get experimentalDecorators(): boolean { + return this._configuration.get<boolean>('implicitProjectConfig.experimentalDecorators', false) + } + + public get disableAutomaticTypeAcquisition(): boolean { + return this._configuration.get<boolean>('disableAutomaticTypeAcquisition', false) + } + + public get formatOnType(): boolean { + return this._configuration.get<boolean>('formatOnType', false) + } + + public get debugPort(): number | null { + return this._configuration.get<number>('debugPort', parseInt(process.env['TSS_DEBUG'], 10)) + } + + public get npmLocation(): string | null { + let path = this._configuration.get<string>('npm', '') + if (path) return path + try { + path = which.sync('npm') + } catch (e) { + return null + } + return path + } + + public static loadFromWorkspace(): TypeScriptServiceConfiguration { + return new TypeScriptServiceConfiguration() + } +} diff --git a/src/server/utils/fs.ts b/src/server/utils/fs.ts new file mode 100644 index 0000000..18cf37f --- /dev/null +++ b/src/server/utils/fs.ts @@ -0,0 +1,30 @@ +import path from 'path' +import os from 'os' +import fs from 'fs' + +export function getParentDirs(fullpath: string): string[] { + let obj = path.parse(fullpath) + if (!obj || !obj.root) return [] + let res = [] + let p = path.dirname(fullpath) + while (p && p !== obj.root) { + res.push(p) + p = path.dirname(p) + } + return res +} + +export function resolveRoot(cwd: string, subs: string[], home?: string): string | null { + home = home || os.homedir() + let { root } = path.parse(cwd) + let paths = getParentDirs(cwd) + paths.unshift(cwd) + for (let p of paths) { + if (p == home || p == root) return null + for (let sub of subs) { + let d = path.join(p, sub) + if (fs.existsSync(d)) return path.dirname(d) + } + } + return root +} diff --git a/src/server/utils/is.ts b/src/server/utils/is.ts new file mode 100644 index 0000000..b247111 --- /dev/null +++ b/src/server/utils/is.ts @@ -0,0 +1,18 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +const toString = Object.prototype.toString + +export function defined(value: any): boolean { + return typeof value !== 'undefined' +} + +export function boolean(value: any): value is boolean { + return value === true || value === false +} + +export function string(value: any): value is string { + return toString.call(value) === '[object String]' +} diff --git a/src/server/utils/languageDescription.ts b/src/server/utils/languageDescription.ts new file mode 100644 index 0000000..e41154c --- /dev/null +++ b/src/server/utils/languageDescription.ts @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as languageModeIds from './languageModeIds' + +export interface LanguageDescription { + readonly id: string + readonly diagnosticSource: string + readonly modeIds: string[] + readonly configFile?: string + readonly isExternal?: boolean + readonly diagnosticOwner: string +} + +export const standardLanguageDescriptions: LanguageDescription[] = [ + { + id: 'typescript', + diagnosticSource: 'ts', + diagnosticOwner: 'typescript', + modeIds: [languageModeIds.typescript, languageModeIds.typescriptreact, + languageModeIds.typescripttsx, languageModeIds.typescriptjsx], + configFile: 'tsconfig.json' + }, + { + id: 'javascript', + diagnosticSource: 'ts', + diagnosticOwner: 'typescript', + modeIds: [languageModeIds.javascript, languageModeIds.javascriptreact], + configFile: 'jsconfig.json' + } +] diff --git a/src/server/utils/languageModeIds.ts b/src/server/utils/languageModeIds.ts new file mode 100644 index 0000000..925fbd4 --- /dev/null +++ b/src/server/utils/languageModeIds.ts @@ -0,0 +1,14 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export const typescript = 'typescript' +export const typescriptreact = 'typescriptreact' +export const typescripttsx = 'typescript.tsx' +export const typescriptjsx = 'typescript.jsx' +export const javascript = 'javascript' +export const javascriptreact = 'javascript.jsx' +export const jsxTags = 'jsx-tags' + +export const languageIds = [typescript, typescriptreact, javascript, javascriptreact, typescripttsx, jsxTags] diff --git a/src/server/utils/logger.ts b/src/server/utils/logger.ts new file mode 100644 index 0000000..5e7d6a8 --- /dev/null +++ b/src/server/utils/logger.ts @@ -0,0 +1,67 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { OutputChannel, workspace } from 'coc.nvim' +import * as is from './is' + +export default class Logger { + + private _channel: OutputChannel + + private get output(): OutputChannel { + if (this._channel) { + return this._channel + } + this._channel = workspace.createOutputChannel('tsserver') + return this._channel + } + + public dispose(): void { + if (this._channel) { + this._channel.dispose() + } + } + + private data2String(data: any): string { + if (data instanceof Error) { + if (is.string(data.stack)) { + return data.stack + } + return (data as Error).message + } + if (is.boolean(data.success) && !data.success && is.string(data.message)) { + return data.message + } + if (is.string(data)) { + return data + } + return data.toString() + } + + public info(message: string, data?: any): void { + this.logLevel('Info', message, data) + } + + public warn(message: string, data?: any): void { + this.logLevel('Warn', message, data) + } + + public error(message: string, data?: any): void { + // See https://github.com/Microsoft/TypeScript/issues/10496 + if (data && data.message === 'No content available.') { + return + } + this.logLevel('Error', message, data) + } + + public logLevel(level: string, message: string, data?: any): void { + this.output.appendLine( + `[${level} - ${new Date().toLocaleTimeString()}] ${message}` + ) + if (data) { + this.output.appendLine(this.data2String(data)) + } + } +} diff --git a/src/server/utils/previewer.ts b/src/server/utils/previewer.ts new file mode 100644 index 0000000..07d53b7 --- /dev/null +++ b/src/server/utils/previewer.ts @@ -0,0 +1,73 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { MarkupContent, MarkupKind } from 'vscode-languageserver-protocol' +import * as Proto from '../protocol' + +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```' + } + + return tag.text +} + +function getTagDocumentation(tag: Proto.JSDocTagInfo): string | undefined { + switch (tag.name) { + case 'param': + const body = (tag.text || '').split(/^([\w\.]+)\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 = getTagBodyText(tag) + if (!text) { + return label + } + return label + (text.match(/\r\n|\n/g) ? '\n' + text : ` — ${text}`) +} + +export function plain(parts: Proto.SymbolDisplayPart[]): string { + if (!parts || !parts.length) return '' + return parts.map(part => part.text).join('') +} + +export function tagsMarkdownPreview(tags: Proto.JSDocTagInfo[]): string { + return (tags || []).map(getTagDocumentation).join(' \n\n') +} + +export function markdownDocumentation( + documentation: Proto.SymbolDisplayPart[], + tags: Proto.JSDocTagInfo[] +): MarkupContent { + let out = plain(documentation) + const tagsPreview = tagsMarkdownPreview(tags) + if (tagsPreview) { + out = out + ('\n\n' + tagsPreview) + } + return { + kind: MarkupKind.Markdown, + value: out + } +} diff --git a/src/server/utils/process.ts b/src/server/utils/process.ts new file mode 100644 index 0000000..7e62387 --- /dev/null +++ b/src/server/utils/process.ts @@ -0,0 +1,154 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import cp from 'child_process' +import net from 'net' +import os from 'os' +import path from 'path' +import { workspace } from 'coc.nvim' +import Logger from './logger' + +export interface IForkOptions { + cwd?: string + execArgv?: string[] +} + +export function makeRandomHexString(length: number): string { + let chars = ['0', '1', '2', '3', '4', '5', '6', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'] + let result = '' + for (let i = 0; i < length; i++) { + const idx = Math.floor(chars.length * Math.random()) + result += chars[idx] + } + return result +} + +function generatePipeName(): string { + return getPipeName(makeRandomHexString(40)) +} + +function getPipeName(name: string): string { + const fullName = 'coc-tsc-' + name + if (process.platform === 'win32') { + return '\\\\.\\pipe\\' + fullName + '-sock' + } + + // Mac/Unix: use socket file + return path.join(os.tmpdir(), fullName + '.sock') +} + +export function getTempFile(name: string): string { + const fullName = 'coc-nvim-' + name + return path.join(os.tmpdir(), fullName + '.sock') +} + +function generatePatchedEnv( + env: any, + stdInPipeName: string, + stdOutPipeName: string, + stdErrPipeName: string +): any { + const newEnv = Object.assign({}, env) + + // Set the two unique pipe names and the electron flag as process env + newEnv['STDIN_PIPE_NAME'] = stdInPipeName // tslint:disable-line + newEnv['STDOUT_PIPE_NAME'] = stdOutPipeName // tslint:disable-line + newEnv['STDERR_PIPE_NAME'] = stdErrPipeName // tslint:disable-line + newEnv['TSS_LOG'] = `-level verbose -file ${path.join(os.tmpdir(), 'coc-nvim-tsc.log')}` // tslint:disable-line + + // Ensure we always have a PATH set + newEnv['PATH'] = newEnv['PATH'] || process.env.PATH // tslint:disable-line + return newEnv +} + +export function fork( + modulePath: string, + args: string[], + options: IForkOptions, + logger: Logger, + callback: (error: any, cp: cp.ChildProcess | null) => void +): void { + let callbackCalled = false + const resolve = (result: cp.ChildProcess) => { + if (callbackCalled) { + return + } + callbackCalled = true + callback(null, result) + } + const reject = (err: any) => { + if (callbackCalled) { + return + } + callbackCalled = true + callback(err, null) + } + + // Generate three unique pipe names + const stdInPipeName = generatePipeName() + const stdOutPipeName = generatePipeName() + const stdErrPipeName = generatePipeName() + + const newEnv = generatePatchedEnv( + process.env, + stdInPipeName, + stdOutPipeName, + stdErrPipeName + ) + newEnv['NODE_PATH'] = path.join(modulePath, '..', '..', '..') // tslint:disable-line + + let childProcess: cp.ChildProcess + // Begin listening to stderr pipe + let stdErrServer = net.createServer(stdErrStream => { + // From now on the childProcess.stderr is available for reading + childProcess.stderr = stdErrStream + }) + stdErrServer.listen(stdErrPipeName) + + // Begin listening to stdout pipe + let stdOutServer = net.createServer(stdOutStream => { + // The child process will write exactly one chunk with content `ready` when it has installed a listener to the stdin pipe + + stdOutStream.once('data', (_chunk: Buffer) => { + // The child process is sending me the `ready` chunk, time to connect to the stdin pipe + childProcess.stdin = net.connect(stdInPipeName) as any + + // From now on the childProcess.stdout is available for reading + childProcess.stdout = stdOutStream + + resolve(childProcess) + }) + }) + stdOutServer.listen(stdOutPipeName) + + let serverClosed = false + const closeServer = () => { + if (serverClosed) { + return + } + serverClosed = true + stdOutServer.close() + stdErrServer.close() + } + + // Create the process + logger.info('Forking TSServer', `PATH: ${newEnv['PATH']} `) + + const bootstrapperPath = path.join(workspace.pluginRoot, 'bin/tsserverForkStart') + childProcess = cp.fork(bootstrapperPath, [modulePath].concat(args), { + silent: true, + env: newEnv, + execArgv: options.execArgv + }) + + childProcess.once('error', (err: Error) => { + closeServer() + reject(err) + }) + + childProcess.once('exit', (err: Error) => { + closeServer() + reject(err) + }) +} diff --git a/src/server/utils/regexp.ts b/src/server/utils/regexp.ts new file mode 100644 index 0000000..274a4b2 --- /dev/null +++ b/src/server/utils/regexp.ts @@ -0,0 +1,8 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export function escapeRegExp(text: string): string { + return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&') +} diff --git a/src/server/utils/tracer.ts b/src/server/utils/tracer.ts new file mode 100644 index 0000000..4583be7 --- /dev/null +++ b/src/server/utils/tracer.ts @@ -0,0 +1,101 @@ +/*--------------------------------------------------------------------------------------------- + * 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 * as Proto from '../protocol' +import Logger from './logger' + +enum Trace { + Off, + Messages, + Verbose +} + +namespace Trace { + export function fromString(value: string): Trace { + value = value || '' + value = value.toLowerCase() + switch (value) { + case 'off': + return Trace.Off + case 'messages': + return Trace.Messages + case 'verbose': + return Trace.Verbose + default: + return Trace.Off + } + } +} + +export default class Tracer { + private trace?: Trace + + constructor(private readonly logger: Logger) { + this.trace = Tracer.readTrace() + } + + private static readTrace(): Trace { + let result: Trace = Trace.fromString(workspace.getConfiguration('tsserver').get<string>('trace.server', 'off')) + if (result === Trace.Off && !!process.env.TSS_TRACE) { + result = Trace.Messages + } + return result + } + + public traceRequest( + request: Proto.Request, + responseExpected: boolean, + queueLength: number + ): void { + if (this.trace === Trace.Off) return + let data: string | undefined + if (this.trace === Trace.Verbose && request.arguments) { + data = `Arguments: ${JSON.stringify(request.arguments, null, 4)}` + } + this.logTrace( + `Sending request: ${request.command} (${ + request.seq + }). Response expected: ${ + responseExpected ? 'yes' : 'no' + }. Current queue length: ${queueLength}`, + data + ) + } + + public traceResponse(response: Proto.Response, startTime: number): void { + if (this.trace === Trace.Off) { + return + } + let data: string | undefined + if (this.trace === Trace.Verbose && response.body) { + data = `Result: ${JSON.stringify(response.body, null, 4)}` + } + this.logTrace( + `Response received: ${response.command} (${ + response.request_seq + }). Request took ${Date.now() - startTime} ms. Success: ${ + response.success + } ${!response.success ? '. Message: ' + response.message : ''}`, + data + ) + } + + public traceEvent(event: Proto.Event): void { + if (this.trace === Trace.Off) { + return + } + let data: string | undefined + if (this.trace === Trace.Verbose && event.body) { + data = `Data: ${JSON.stringify(event.body, null, 4)}` + } + this.logTrace(`Event received: ${event.event} (${event.seq}).`, data) + } + + public logTrace(message: string, data?: any): void { + if (this.trace !== Trace.Off) { + this.logger.logLevel('Trace', message, data) + } + } +} diff --git a/src/server/utils/tsconfig.ts b/src/server/utils/tsconfig.ts new file mode 100644 index 0000000..01e50f2 --- /dev/null +++ b/src/server/utils/tsconfig.ts @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as Proto from '../protocol' +import { TypeScriptServiceConfiguration } from './configuration' + +export function inferredProjectConfig( + config: TypeScriptServiceConfiguration +): Proto.ExternalProjectCompilerOptions { + const base: Proto.ExternalProjectCompilerOptions = { + module: 'commonjs' as Proto.ModuleKind, + target: 'es2016' as Proto.ScriptTarget, + jsx: 'preserve' as Proto.JsxEmit + } + + if (config.checkJs) { + base.checkJs = true + } + + if (config.experimentalDecorators) { + base.experimentalDecorators = true + } + + return base +} diff --git a/src/server/utils/typeConverters.ts b/src/server/utils/typeConverters.ts new file mode 100644 index 0000000..0906bf5 --- /dev/null +++ b/src/server/utils/typeConverters.ts @@ -0,0 +1,91 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +/** + * Helpers for converting FROM LanguageServer types language-server ts types + */ +import * as language from 'vscode-languageserver-protocol' +import Proto from '../protocol' +import { ITypeScriptServiceClient } from '../typescriptService' + +export namespace Range { + export const fromTextSpan = (span: Proto.TextSpan): language.Range => { + return { + start: { + line: span.start.line - 1, + character: span.start.offset - 1 + }, + end: { + line: span.end.line - 1, + character: span.end.offset - 1 + } + } + } + + export const toFileRangeRequestArgs = ( + file: string, + range: language.Range + ): Proto.FileRangeRequestArgs => ({ + file, + startLine: range.start.line + 1, + startOffset: range.start.character + 1, + endLine: range.end.line + 1, + endOffset: range.end.character + 1 + }) +} + +export namespace Position { + export const fromLocation = (tslocation: Proto.Location): language.Position => { + return { + line: tslocation.line - 1, + character: tslocation.offset - 1 + } + } + + export const toFileLocationRequestArgs = ( + file: string, + position: language.Position + ): Proto.FileLocationRequestArgs => ({ + file, + line: position.line + 1, + offset: position.character + 1 + }) +} + +export namespace Location { + export const fromTextSpan = ( + uri: string, + tsTextSpan: Proto.TextSpan + ): language.Location => { + return { + uri, + range: Range.fromTextSpan(tsTextSpan) + } + } +} + +export namespace TextEdit { + export const fromCodeEdit = (edit: Proto.CodeEdit): language.TextEdit => { + return { + range: Range.fromTextSpan(edit), + newText: edit.newText + } + } +} + +export namespace WorkspaceEdit { + export function fromFileCodeEdits( + client: ITypeScriptServiceClient, + edits: Iterable<Proto.FileCodeEdits> + ): language.WorkspaceEdit { + let changes = {} + for (const edit of edits) { + let uri = client.toResource(edit.fileName) + changes[uri] = edit.textChanges.map(change => { + return TextEdit.fromCodeEdit(change) + }) + } + return { changes } + } +} diff --git a/src/server/utils/typingsStatus.ts b/src/server/utils/typingsStatus.ts new file mode 100644 index 0000000..60418dd --- /dev/null +++ b/src/server/utils/typingsStatus.ts @@ -0,0 +1,115 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { Disposable } from 'vscode-languageserver-protocol' +import { ITypeScriptServiceClient } from '../typescriptService' +import { workspace } from 'coc.nvim' + +const typingsInstallTimeout = 30 * 1000 + +export default class TypingsStatus implements Disposable { + private _acquiringTypings: { [eventId: string]: NodeJS.Timer } = Object.create( + {} + ) + private _client: ITypeScriptServiceClient + private _subscriptions: Disposable[] = [] + + constructor(client: ITypeScriptServiceClient) { + this._client = client + this._subscriptions.push( + this._client.onDidBeginInstallTypings(event => + this.onBeginInstallTypings(event.eventId) + ) + ) + + this._subscriptions.push( + this._client.onDidEndInstallTypings(event => + this.onEndInstallTypings(event.eventId) + ) + ) + } + + public dispose(): void { + this._subscriptions.forEach(x => x.dispose()) + + for (const eventId of Object.keys(this._acquiringTypings)) { + clearTimeout(this._acquiringTypings[eventId]) + } + } + + public get isAcquiringTypings(): boolean { + return Object.keys(this._acquiringTypings).length > 0 + } + + private onBeginInstallTypings(eventId: number): void { + if (this._acquiringTypings[eventId]) { + return + } + this._acquiringTypings[eventId] = setTimeout(() => { + this.onEndInstallTypings(eventId) + }, typingsInstallTimeout) + } + + private onEndInstallTypings(eventId: number): void { + const timer = this._acquiringTypings[eventId] + if (timer) { + clearTimeout(timer) + } + delete this._acquiringTypings[eventId] + } +} + +export class AtaProgressReporter { + private _promises = new Map<number, Function>() + private _disposable: Disposable + private _invalid = false + + constructor(client: ITypeScriptServiceClient) { + const disposables: Disposable[] = [] + disposables.push(client.onDidBeginInstallTypings(e => this._onBegin(e.eventId))) + disposables.push(client.onDidEndInstallTypings(e => this._onEndOrTimeout(e.eventId))) + disposables.push(client.onTypesInstallerInitializationFailed(_ => + this.onTypesInstallerInitializationFailed() + )) + this._disposable = Disposable.create(() => { + disposables.forEach(disposable => { + disposable.dispose() + }) + }) + } + + public dispose(): void { + this._disposable.dispose() + this._promises.forEach(value => value()) + } + + private _onBegin(eventId: number): void { + const handle = setTimeout( + () => this._onEndOrTimeout(eventId), + typingsInstallTimeout + ) + new Promise(resolve => { // tslint:disable-line + this._promises.set(eventId, () => { + clearTimeout(handle) + resolve() + }) + }) + workspace.showMessage('Fetching data for better TypeScript IntelliSense') + } + + private _onEndOrTimeout(eventId: number): void { + const resolve = this._promises.get(eventId) + if (resolve) { + this._promises.delete(eventId) + resolve() + } + } + + private onTypesInstallerInitializationFailed() { // tslint:disable-line + if (!this._invalid) { + workspace.showMessage('Could not install typings files for JavaScript language features. Please ensure that NPM is installed', 'error') + } + this._invalid = true + } +} diff --git a/src/server/utils/versionProvider.ts b/src/server/utils/versionProvider.ts new file mode 100644 index 0000000..66911fd --- /dev/null +++ b/src/server/utils/versionProvider.ts @@ -0,0 +1,137 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import fs from 'fs' +import path from 'path' +import { getParentDirs } from './fs' +import { workspace } from 'coc.nvim' +import API from './api' +import { TypeScriptServiceConfiguration } from './configuration' + +export class TypeScriptVersion { + private _api: API | null | undefined + constructor( + public readonly path: string, + private readonly _pathLabel?: string + ) { + this._api = null + } + + public get tsServerPath(): string { + return path.join(this.path, 'tsserver.js') + } + + public get pathLabel(): string { + return typeof this._pathLabel === 'undefined' ? this.path : this._pathLabel + } + + public get isValid(): boolean { + return this.version != null + } + + public get version(): API | null { + if (this._api) return this._api + let api = this._api = this.getTypeScriptVersion(this.tsServerPath) + return api + } + + public get versionString(): string | null { + const version = this.version + return version ? version.versionString : null + + } + + private getTypeScriptVersion(serverPath: string): API | undefined { + if (!fs.existsSync(serverPath)) { + return undefined + } + + const p = serverPath.split(path.sep) + if (p.length <= 2) { + return undefined + } + const p2 = p.slice(0, -2) + const modulePath = p2.join(path.sep) + let fileName = path.join(modulePath, 'package.json') + if (!fs.existsSync(fileName)) { + // Special case for ts dev versions + if (path.basename(modulePath) === 'built') { + fileName = path.join(modulePath, '..', 'package.json') + } + } + if (!fs.existsSync(fileName)) { + return undefined + } + + const contents = fs.readFileSync(fileName).toString() + let desc: any = null + try { + desc = JSON.parse(contents) + } catch (err) { + return undefined + } + if (!desc || !desc.version) { + return undefined + } + return desc.version ? API.fromVersionString(desc.version) : undefined + } +} + +export class TypeScriptVersionProvider { + + public constructor(private configuration: TypeScriptServiceConfiguration) { } + + public updateConfiguration( + configuration: TypeScriptServiceConfiguration + ): void { + this.configuration = configuration + } + + public async getDefaultVersion(): Promise<TypeScriptVersion> { + // tsdk from configuration + let { globalTsdk } = this.configuration + if (globalTsdk) return new TypeScriptVersion(globalTsdk) + // resolve global module + let modulePath = await workspace.resolveModule('typescript', 'tsserver') + if (modulePath) { + let p = path.join(modulePath, 'lib') + return new TypeScriptVersion(p) + } + // use bundled + return this.bundledVersion + } + + public get globalVersion(): TypeScriptVersion | undefined { + let { globalTsdk } = this.configuration + if (globalTsdk) return new TypeScriptVersion(globalTsdk) + return undefined + } + + public getLocalVersion(root): TypeScriptVersion | undefined { + let paths = getParentDirs(root) + paths.unshift(root) + for (let p of paths) { + if (fs.existsSync(path.join(p, 'node_modules'))) { + let lib = path.join(p, 'node_modules/typescript/lib') + return new TypeScriptVersion(lib) + } + } + return null + } + + public get bundledVersion(): TypeScriptVersion | null { + let file = path.join(workspace.pluginRoot, 'node_modules/typescript/lib/tsserver.js') + if (!fs.existsSync(file)) return null + try { + const bundledVersion = new TypeScriptVersion( + path.dirname(file), + '' + ) + return bundledVersion + } catch (e) { + // noop + } + return null + } +} diff --git a/src/server/utils/wireProtocol.ts b/src/server/utils/wireProtocol.ts new file mode 100644 index 0000000..f71b11f --- /dev/null +++ b/src/server/utils/wireProtocol.ts @@ -0,0 +1,138 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import stream from 'stream' + +const DefaultSize = 8192 +const ContentLength = 'Content-Length: ' +const ContentLengthSize: number = Buffer.byteLength(ContentLength, 'utf8') +const Blank: number = Buffer.from(' ', 'utf8')[0] +const BackslashR: number = Buffer.from('\r', 'utf8')[0] +const BackslashN: number = Buffer.from('\n', 'utf8')[0] + +class ProtocolBuffer { + private index = 0 + private buffer: Buffer = Buffer.allocUnsafe(DefaultSize) + + public append(data: string | Buffer): void { + let toAppend: Buffer | null = null + if (Buffer.isBuffer(data)) { + toAppend = data as Buffer + } else { + toAppend = Buffer.from(data, 'utf8') + } + if (this.buffer.length - this.index >= toAppend.length) { + toAppend.copy(this.buffer, this.index, 0, toAppend.length) + } else { + let newSize = + (Math.ceil((this.index + toAppend.length) / DefaultSize) + 1) * + DefaultSize + if (this.index === 0) { + this.buffer = Buffer.allocUnsafe(newSize) + toAppend.copy(this.buffer, 0, 0, toAppend.length) + } else { + this.buffer = Buffer.concat( + [this.buffer.slice(0, this.index), toAppend], + newSize + ) + } + } + this.index += toAppend.length + } + + public tryReadContentLength(): number { + let result = -1 + let current = 0 + // we are utf8 encoding... + while ( + current < this.index && + (this.buffer[current] === Blank || + this.buffer[current] === BackslashR || + this.buffer[current] === BackslashN) + ) { + current++ + } + if (this.index < current + ContentLengthSize) { + return result + } + current += ContentLengthSize + let start = current + while (current < this.index && this.buffer[current] !== BackslashR) { + current++ + } + if ( + current + 3 >= this.index || + this.buffer[current + 1] !== BackslashN || + this.buffer[current + 2] !== BackslashR || + this.buffer[current + 3] !== BackslashN + ) { + return result + } + let data = this.buffer.toString('utf8', start, current) + result = parseInt(data, 10) + this.buffer = this.buffer.slice(current + 4) + this.index = this.index - (current + 4) + return result + } + + public tryReadContent(length: number): string | null { + if (this.index < length) { + return null + } + let result = this.buffer.toString('utf8', 0, length) + let sourceStart = length + while ( + sourceStart < this.index && + (this.buffer[sourceStart] === BackslashR || + this.buffer[sourceStart] === BackslashN) + ) { + sourceStart++ + } + this.buffer.copy(this.buffer, 0, sourceStart) + this.index = this.index - sourceStart + return result + } +} + +export interface ICallback<T> { + (data: T): void // tslint:disable-line +} + +export class Reader<T> { + private readonly buffer: ProtocolBuffer = new ProtocolBuffer() + private nextMessageLength = -1 + + public constructor( + private readonly readable: stream.Readable, + private readonly callback: ICallback<T>, + private readonly onError: (error: any) => void + ) { + this.readable.on('data', (data: Buffer) => { + this.onLengthData(data) + }) + } + + private onLengthData(data: Buffer): void { + try { + this.buffer.append(data) + while (true) { + if (this.nextMessageLength === -1) { + this.nextMessageLength = this.buffer.tryReadContentLength() + if (this.nextMessageLength === -1) { + return + } + } + const msg = this.buffer.tryReadContent(this.nextMessageLength) + if (msg === null) { + return + } + this.nextMessageLength = -1 + const json = JSON.parse(msg) + this.callback(json) + } + } catch (e) { + this.onError(e) + } + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..4a9578e --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "./node_modules/@chemzqm/tsconfig/tsconfig.json", + "compilerOptions": { + "outDir": "lib", + "target": "es2015", + "module": "commonjs", + "moduleResolution": "node", + "noImplicitThis": true, + "importHelpers": true, + "lib": ["es2018"], + "plugins": [] + }, + "include": [ + "src" + ], + "exclude": [ + ] +} diff --git a/tslint.json b/tslint.json new file mode 100644 index 0000000..3bb26c0 --- /dev/null +++ b/tslint.json @@ -0,0 +1,10 @@ +{ + "extends": "./node_modules/@chemzqm/tslint-config/tslint.json", + "rules": { + }, + "linterOptions": { + "exclude": [ + ] + } +} + diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..c3c55c8 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,644 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/code-frame@7.0.0-beta.44": + version "7.0.0-beta.44" + resolved "http://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0-beta.44.tgz#2a02643368de80916162be70865c97774f3adbd9" + dependencies: + "@babel/highlight" "7.0.0-beta.44" + +"@babel/generator@7.0.0-beta.44": + version "7.0.0-beta.44" + resolved "http://registry.npmjs.org/@babel/generator/-/generator-7.0.0-beta.44.tgz#c7e67b9b5284afcf69b309b50d7d37f3e5033d42" + dependencies: + "@babel/types" "7.0.0-beta.44" + jsesc "^2.5.1" + lodash "^4.2.0" + source-map "^0.5.0" + trim-right "^1.0.1" + +"@babel/helper-function-name@7.0.0-beta.44": + version "7.0.0-beta.44" + resolved "http://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.0.0-beta.44.tgz#e18552aaae2231100a6e485e03854bc3532d44dd" + dependencies: + "@babel/helper-get-function-arity" "7.0.0-beta.44" + "@babel/template" "7.0.0-beta.44" + "@babel/types" "7.0.0-beta.44" + +"@babel/helper-get-function-arity@7.0.0-beta.44": + version "7.0.0-beta.44" + resolved "http://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0-beta.44.tgz#d03ca6dd2b9f7b0b1e6b32c56c72836140db3a15" + dependencies: + "@babel/types" "7.0.0-beta.44" + +"@babel/helper-split-export-declaration@7.0.0-beta.44": + version "7.0.0-beta.44" + resolved "http://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0-beta.44.tgz#c0b351735e0fbcb3822c8ad8db4e583b05ebd9dc" + dependencies: + "@babel/types" "7.0.0-beta.44" + +"@babel/highlight@7.0.0-beta.44": + version "7.0.0-beta.44" + resolved "http://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0-beta.44.tgz#18c94ce543916a80553edcdcf681890b200747d5" + dependencies: + chalk "^2.0.0" + esutils "^2.0.2" + js-tokens "^3.0.0" + +"@babel/template@7.0.0-beta.44": + version "7.0.0-beta.44" + resolved "http://registry.npmjs.org/@babel/template/-/template-7.0.0-beta.44.tgz#f8832f4fdcee5d59bf515e595fc5106c529b394f" + dependencies: + "@babel/code-frame" "7.0.0-beta.44" + "@babel/types" "7.0.0-beta.44" + babylon "7.0.0-beta.44" + lodash "^4.2.0" + +"@babel/traverse@7.0.0-beta.44": + version "7.0.0-beta.44" + resolved "http://registry.npmjs.org/@babel/traverse/-/traverse-7.0.0-beta.44.tgz#a970a2c45477ad18017e2e465a0606feee0d2966" + dependencies: + "@babel/code-frame" "7.0.0-beta.44" + "@babel/generator" "7.0.0-beta.44" + "@babel/helper-function-name" "7.0.0-beta.44" + "@babel/helper-split-export-declaration" "7.0.0-beta.44" + "@babel/types" "7.0.0-beta.44" + babylon "7.0.0-beta.44" + debug "^3.1.0" + globals "^11.1.0" + invariant "^2.2.0" + lodash "^4.2.0" + +"@babel/types@7.0.0-beta.44": + version "7.0.0-beta.44" + resolved "http://registry.npmjs.org/@babel/types/-/types-7.0.0-beta.44.tgz#6b1b164591f77dec0a0342aca995f2d046b3a757" + dependencies: + esutils "^2.0.2" + lodash "^4.2.0" + to-fast-properties "^2.0.0" + +"@chemzqm/neovim@4.3.23": + version "4.3.23" + resolved "https://registry.yarnpkg.com/@chemzqm/neovim/-/neovim-4.3.23.tgz#85385db1bedde01593be5f5ea7cbf7db10a868aa" + dependencies: + babel-eslint "^8.2.6" + msgpack-lite "^0.1.26" + traverse "^0.6.6" + +"@chemzqm/tsconfig@^0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@chemzqm/tsconfig/-/tsconfig-0.0.3.tgz#ce3480d15d8cec46a315488caa07c9fca819aecc" + +"@chemzqm/tslint-config@^1.0.17": + version "1.0.17" + resolved "https://registry.yarnpkg.com/@chemzqm/tslint-config/-/tslint-config-1.0.17.tgz#9365dc9bdece0927fdfa6c068e1c70524c35a5fc" + dependencies: + tslint-config-prettier "^1.6.0" + tslint-react "^3.2.0" + +"@types/node@^10.9.4": + version "10.9.4" + resolved "https://registry.yarnpkg.com/@types/node/-/node-10.9.4.tgz#0f4cb2dc7c1de6096055357f70179043c33e9897" + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + +ansi-styles@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + dependencies: + color-convert "^1.9.0" + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + dependencies: + sprintf-js "~1.0.2" + +babel-code-frame@^6.22.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" + dependencies: + chalk "^1.1.3" + esutils "^2.0.2" + js-tokens "^3.0.2" + +babel-eslint@^8.2.6: + version "8.2.6" + resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-8.2.6.tgz#6270d0c73205628067c0f7ae1693a9e797acefd9" + dependencies: + "@babel/code-frame" "7.0.0-beta.44" + "@babel/traverse" "7.0.0-beta.44" + "@babel/types" "7.0.0-beta.44" + babylon "7.0.0-beta.44" + eslint-scope "3.7.1" + eslint-visitor-keys "^1.0.0" + +babylon@7.0.0-beta.44: + version "7.0.0-beta.44" + resolved "http://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.44.tgz#89159e15e6e30c5096e22d738d8c0af8a0e8ca1d" + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +bser@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/bser/-/bser-2.0.0.tgz#9ac78d3ed5d915804fd87acb158bc797147a1719" + dependencies: + node-int64 "^0.4.0" + +builtin-modules@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" + +chalk@^1.1.3: + version "1.1.3" + resolved "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" + dependencies: + ansi-styles "^2.2.1" + escape-string-regexp "^1.0.2" + has-ansi "^2.0.0" + strip-ansi "^3.0.0" + supports-color "^2.0.0" + +chalk@^2.0.0, chalk@^2.3.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e" + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +circular-json@^0.5.5: + version "0.5.5" + resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.5.5.tgz#64182ef359042d37cd8e767fc9de878b1e9447d3" + +coc.nvim@^0.0.15: + version "0.0.15" + resolved "https://registry.yarnpkg.com/coc.nvim/-/coc.nvim-0.0.15.tgz#e28e42e9dae92bf372b16918bf68b2e8696e7e4d" + dependencies: + "@chemzqm/neovim" "4.3.23" + debounce "^1.2.0" + deep-equal "^1.0.1" + diff "^3.5.0" + fast-diff "^1.1.2" + fb-watchman "^2.0.0" + fuzzaldrin "^2.1.0" + glob "^7.1.3" + jsonc-parser "^2.0.2" + log4js "^3.0.5" + minimatch "^3.0.4" + node-serial "^0.1.1" + once "^1.4.0" + pify "^4.0.0" + semver "^5.5.1" + tslib "^1.9.3" + uuid "^3.3.2" + vscode-languageserver-protocol "^3.12.0" + vscode-languageserver-types "^3.12.0" + vscode-uri "^1.0.6" + which "^1.3.1" + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + dependencies: + color-name "1.1.3" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + +commander@^2.12.1: + version "2.17.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + +core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + +date-format@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/date-format/-/date-format-1.2.0.tgz#615e828e233dd1ab9bb9ae0950e0ceccfa6ecad8" + +debounce@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.0.tgz#44a540abc0ea9943018dc0eaa95cce87f65cd131" + +debug@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + dependencies: + ms "2.0.0" + +deep-equal@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" + +diff@^3.2.0, diff@^3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" + +escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + +eslint-scope@3.7.1: + version "3.7.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-3.7.1.tgz#3d63c3edfda02e06e01a452ad88caacc7cdcb6e8" + dependencies: + esrecurse "^4.1.0" + estraverse "^4.1.1" + +eslint-visitor-keys@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d" + +esprima@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + +esrecurse@^4.1.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf" + dependencies: + estraverse "^4.1.0" + +estraverse@^4.1.0, estraverse@^4.1.1: + version "4.2.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" + +esutils@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" + +event-lite@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/event-lite/-/event-lite-0.1.1.tgz#47cf08a8d37d0b694cdb7b3b17b51faac6576086" + +fast-diff@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.1.2.tgz#4b62c42b8e03de3f848460b639079920695d0154" + +fb-watchman@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.0.tgz#54e9abf7dfa2f26cd9b1636c588c1afc05de5d58" + dependencies: + bser "^2.0.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + +fuzzaldrin@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fuzzaldrin/-/fuzzaldrin-2.1.0.tgz#90204c3e2fdaa6941bb28d16645d418063a90e9b" + +glob@^7.0.5, glob@^7.1.1, glob@^7.1.3: + version "7.1.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globals@^11.1.0: + version "11.7.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.7.0.tgz#a583faa43055b1aca771914bf68258e2fc125673" + +has-ansi@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" + dependencies: + ansi-regex "^2.0.0" + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + +ieee754@^1.1.8: + version "1.1.12" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.12.tgz#50bf24e5b9c8bb98af4964c941cdb0918da7b60b" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@~2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + +int64-buffer@^0.1.9: + version "0.1.10" + resolved "https://registry.yarnpkg.com/int64-buffer/-/int64-buffer-0.1.10.tgz#277b228a87d95ad777d07c13832022406a473423" + +invariant@^2.2.0: + version "2.2.4" + resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" + dependencies: + loose-envify "^1.0.0" + +isarray@^1.0.0, isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + +js-tokens@^3.0.0, js-tokens@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" + +"js-tokens@^3.0.0 || ^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + +js-yaml@^3.7.0: + version "3.12.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.0.tgz#eaed656ec8344f10f527c6bfa1b6e2244de167d1" + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +jsesc@^2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.1.tgz#e421a2a8e20d6b0819df28908f782526b96dd1fe" + +jsonc-parser@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.0.2.tgz#42fcf56d70852a043fadafde51ddb4a85649978d" + +lodash@^4.2.0: + version "4.17.10" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" + +log4js@^3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/log4js/-/log4js-3.0.5.tgz#b80146bfebad68b430d4f3569556d8a6edfef303" + dependencies: + circular-json "^0.5.5" + date-format "^1.2.0" + debug "^3.1.0" + rfdc "^1.1.2" + streamroller "0.7.0" + +loose-envify@^1.0.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + dependencies: + brace-expansion "^1.1.7" + +minimist@0.0.8: + version "0.0.8" + resolved "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + +mkdirp@^0.5.1: + version "0.5.1" + resolved "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + dependencies: + minimist "0.0.8" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + +msgpack-lite@^0.1.26: + version "0.1.26" + resolved "https://registry.yarnpkg.com/msgpack-lite/-/msgpack-lite-0.1.26.tgz#dd3c50b26f059f25e7edee3644418358e2a9ad89" + dependencies: + event-lite "^0.1.1" + ieee754 "^1.1.8" + int64-buffer "^0.1.9" + isarray "^1.0.0" + +next-tick@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-0.1.0.tgz#1912cce8eb9b697d640fbba94f8f00dec3b94259" + +node-int64@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" + +node-serial@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/node-serial/-/node-serial-0.1.1.tgz#3766fa9614c20c6f27d3713f9bd8c56747fbb8c9" + dependencies: + next-tick "~0.1.0" + +once@^1.3.0, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + dependencies: + wrappy "1" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + +path-parse@^1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" + +pify@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.0.tgz#db04c982b632fd0df9090d14aaf1c8413cadb695" + +process-nextick-args@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" + +readable-stream@^2.3.0: + version "2.3.6" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +resolve@^1.3.2: + version "1.8.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.8.1.tgz#82f1ec19a423ac1fbd080b0bab06ba36e84a7a26" + dependencies: + path-parse "^1.0.5" + +rfdc@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.1.2.tgz#e6e72d74f5dc39de8f538f65e00c36c18018e349" + +rimraf@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" + dependencies: + glob "^7.0.5" + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + +semver@^5.3.0, semver@^5.5.1: + version "5.5.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.1.tgz#7dfdd8814bdb7cabc7be0fb1d734cfb66c940477" + +source-map@^0.5.0: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + +streamroller@0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-0.7.0.tgz#a1d1b7cf83d39afb0d63049a5acbf93493bdf64b" + dependencies: + date-format "^1.2.0" + debug "^3.1.0" + mkdirp "^0.5.1" + readable-stream "^2.3.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + dependencies: + ansi-regex "^2.0.0" + +supports-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + dependencies: + has-flag "^3.0.0" + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + +traverse@^0.6.6: + version "0.6.6" + resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.6.6.tgz#cbdf560fd7b9af632502fed40f918c157ea97137" + +trim-right@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" + +tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.3: + version "1.9.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" + +tslint-config-prettier@^1.6.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/tslint-config-prettier/-/tslint-config-prettier-1.15.0.tgz#76b9714399004ab6831fdcf76d89b73691c812cf" + +tslint-react@^3.2.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/tslint-react/-/tslint-react-3.6.0.tgz#7f462c95c4a0afaae82507f06517ff02942196a1" + dependencies: + tsutils "^2.13.1" + +tslint@^5.11.0: + version "5.11.0" + resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.11.0.tgz#98f30c02eae3cde7006201e4c33cb08b48581eed" + dependencies: + babel-code-frame "^6.22.0" + builtin-modules "^1.1.1" + chalk "^2.3.0" + commander "^2.12.1" + diff "^3.2.0" + glob "^7.1.1" + js-yaml "^3.7.0" + minimatch "^3.0.4" + resolve "^1.3.2" + semver "^5.3.0" + tslib "^1.8.0" + tsutils "^2.27.2" + +tsutils@^2.13.1, tsutils@^2.27.2: + version "2.29.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.29.0.tgz#32b488501467acbedd4b85498673a0812aca0b99" + dependencies: + tslib "^1.8.1" + +typescript@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.0.3.tgz#4853b3e275ecdaa27f78fda46dc273a7eb7fc1c8" + +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + +uuid@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" + +vscode-jsonrpc@^3.6.2: + version "3.6.2" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-3.6.2.tgz#3b5eef691159a15556ecc500e9a8a0dd143470c8" + +vscode-languageserver-protocol@^3.12.0: + version "3.12.0" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.12.0.tgz#5b23501292abad88f0463b01e83ff98e64a37652" + dependencies: + vscode-jsonrpc "^3.6.2" + vscode-languageserver-types "^3.12.0" + +vscode-languageserver-types@^3.12.0: + version "3.12.0" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.12.0.tgz#f96051381b6a050b7175b37d6cb5d2f2eb64b944" + +vscode-uri@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-1.0.6.tgz#6b8f141b0bbc44ad7b07e94f82f168ac7608ad4d" + +which@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + dependencies: + isexe "^2.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"