From ef0beffe37edc2fa6a905036b546b65d32e81af2 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Sat, 4 Nov 2017 22:44:11 -0700 Subject: [PATCH 01/17] style: use prettier --- README.md | 25 +- package.json | 10 +- prettier.config.js | 1 + src/ast.ts | 3 +- src/connection.ts | 78 +- src/diagnostics.ts | 4 +- src/disposable.ts | 1 - src/fs.ts | 54 +- src/lang-handler.ts | 21 +- src/language-server-stdio.ts | 12 +- src/language-server.ts | 10 +- src/logging.ts | 5 +- src/match-files.ts | 502 ++-- src/memfs.ts | 12 +- src/packages.ts | 95 +- src/plugins.ts | 31 +- src/project-manager.ts | 270 +- src/request-type.ts | 8 - src/server.ts | 11 +- src/symbols.ts | 92 +- src/test/connection.test.ts | 204 +- src/test/fs-helpers.ts | 6 +- src/test/fs.test.ts | 18 +- src/test/plugins.test.ts | 18 +- src/test/project-manager.test.ts | 88 +- src/test/tracing.test.ts | 81 +- src/test/typescript-service-helpers.ts | 3517 ++++++++++++++---------- src/test/typescript-service.test.ts | 1 - src/test/util-test.ts | 170 +- src/tracing.ts | 21 +- src/typescript-service.ts | 1099 +++++--- src/typings/string-similarity.d.ts | 1 - tsfmt.json | 15 - 33 files changed, 3725 insertions(+), 2759 deletions(-) create mode 100644 prettier.config.js delete mode 100644 tsfmt.json diff --git a/README.md b/README.md index d66e0370d..3a99a8e33 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,17 @@ # JavaScript/TypeScript language server [![npm](https://img.shields.io/npm/v/javascript-typescript-langserver.svg)](https://www.npmjs.com/package/javascript-typescript-langserver) -[![npm](https://img.shields.io/npm/dm/javascript-typescript-langserver.svg)](https://www.npmjs.com/package/javascript-typescript-langserver) -[![Build Status](https://travis-ci.org/sourcegraph/javascript-typescript-langserver.svg?branch=master)](https://travis-ci.org/sourcegraph/javascript-typescript-langserver) -[![Windows Build Status](https://ci.appveyor.com/api/projects/status/2wj7xe035pm7r76v/branch/master?svg=true +[![downloads](https://img.shields.io/npm/dm/javascript-typescript-langserver.svg)](https://www.npmjs.com/package/javascript-typescript-langserver) +[![build](https://travis-ci.org/sourcegraph/javascript-typescript-langserver.svg?branch=master)](https://travis-ci.org/sourcegraph/javascript-typescript-langserver) +[![appveyor build](https://ci.appveyor.com/api/projects/status/2wj7xe035pm7r76v/branch/master?svg=true )](https://ci.appveyor.com/project/sourcegraph/javascript-typescript-langserver/branch/master) [![codecov](https://codecov.io/gh/sourcegraph/javascript-typescript-langserver/branch/master/graph/badge.svg)](https://codecov.io/gh/sourcegraph/javascript-typescript-langserver) -[![Dependencies](https://david-dm.org/sourcegraph/javascript-typescript-langserver.svg)](https://david-dm.org/sourcegraph/javascript-typescript-langserver) +[![dependencies](https://david-dm.org/sourcegraph/javascript-typescript-langserver.svg)](https://david-dm.org/sourcegraph/javascript-typescript-langserver) +[![OpenTracing: enabled](https://img.shields.io/badge/OpenTracing-enabled-blue.svg)](http://opentracing.io) [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) -[![OpenTracing Badge](https://img.shields.io/badge/OpenTracing-enabled-blue.svg)](http://opentracing.io) -[![License](https://img.shields.io/github/license/sourcegraph/javascript-typescript-langserver.svg)]() -[![Gitter](https://badges.gitter.im/sourcegraph/javascript-typescript-langserver.svg)](https://gitter.im/sourcegraph/javascript-typescript-langserver?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) +[![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier) +[![license](https://img.shields.io/github/license/sourcegraph/javascript-typescript-langserver.svg)]() +[![chat: on gitter](https://badges.gitter.im/sourcegraph/javascript-typescript-langserver.svg)](https://gitter.im/sourcegraph/javascript-typescript-langserver?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) This is a language server for JavaScript and TypeScript that adheres to the [Language Server Protocol (LSP)](https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md). It uses [TypeScript's](http://www.typescriptlang.org/) LanguageService to perform source code analysis. @@ -76,15 +77,15 @@ npm test This language server implements some LSP extensions, prefixed with an `x`. -- **[Files extension](https://github.com/sourcegraph/language-server-protocol/blob/master/extension-files.md)** +- **[Files extension](https://github.com/sourcegraph/language-server-protocol/blob/master/extension-files.md)** Allows the server to request file contents without accessing the file system -- **[SymbolDescriptor extension](https://github.com/sourcegraph/language-server-protocol/blob/master/extension-workspace-references.md)** +- **[SymbolDescriptor extension](https://github.com/sourcegraph/language-server-protocol/blob/master/extension-workspace-references.md)** Get a SymbolDescriptor for a symbol, search the workspace for symbols or references to it -- **[Streaming](https://github.com/sourcegraph/language-server-protocol/blob/streaming/protocol.md#partialResult)** +- **[Streaming](https://github.com/sourcegraph/language-server-protocol/blob/streaming/protocol.md#partialResult)** Supports streaming partial results for all endpoints through JSON Patches -- **Packages extension** +- **Packages extension** Methods to get information about dependencies -- **TCP / multiple client support** +- **TCP / multiple client support** When running over TCP, the `exit` notification will not kill the process, but close the TCP socket ## Versioning diff --git a/package.json b/package.json index 8a71f409f..1e1e2d9bc 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,9 @@ "clean": "rimraf lib", "cover": "nyc --silent --all --require source-map-support/register mocha --timeout 7000 --slow 2000 lib/test/**/*.js", "test": "mocha --require source-map-support/register --timeout 7000 --slow 2000 lib/test/**/*.js", - "lint": "tslint -c tslint.json -p .", + "lint": "npm run tslint && npm run prettier", + "tslint": "tslint -c tslint.json -p .", + "prettier": "prettier --list-different --write \"src/**/*.ts\"", "build": "tsc", "watch": "tsc -w", "semantic-release": "semantic-release pre && npm publish && semantic-release post", @@ -55,8 +57,9 @@ "vscode-languageserver-types": "^3.0.3" }, "devDependencies": { + "@sourcegraph/prettierrc": "^1.1.0", "@sourcegraph/tsconfig": "^1.0.0", - "@sourcegraph/tslint-config": "^6.0.0", + "@sourcegraph/tslint-config": "^7.0.0", "@types/chai": "^4.0.0", "@types/chai-as-promised": "^7.1.0", "@types/glob": "^5.0.30", @@ -73,12 +76,13 @@ "husky": "^0.14.0", "mocha": "^4.0.0", "nyc": "^11.0.2", + "prettier": "^1.7.4", "rimraf": "^2.6.1", "semantic-release": "^8.0.0", "sinon": "^4.0.0", "source-map-support": "^0.5.0", "temp": "^0.8.3", - "tslint": "^5.7.0", + "tslint": "^5.8.0", "tslint-language-service": "^0.9.6", "validate-commit-msg": "^2.12.2" }, diff --git a/prettier.config.js b/prettier.config.js new file mode 100644 index 000000000..575c1e944 --- /dev/null +++ b/prettier.config.js @@ -0,0 +1 @@ +module.exports = require('@sourcegraph/prettierrc') diff --git a/src/ast.ts b/src/ast.ts index ef9e6d21c..3a4599b3a 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -1,4 +1,3 @@ - import * as ts from 'typescript' /** @@ -6,7 +5,7 @@ import * as ts from 'typescript' * * TODO is this function worth it? */ -export function *walkMostAST(node: ts.Node): IterableIterator { +export function* walkMostAST(node: ts.Node): IterableIterator { yield node const children = node.getChildren() for (const child of children) { diff --git a/src/connection.ts b/src/connection.ts index 7ea482d3e..5d3a9d58b 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -4,8 +4,20 @@ import { camelCase, omit } from 'lodash' import { FORMAT_TEXT_MAP, SpanContext, Tracer } from 'opentracing' import { Observable, Subscription, Symbol } from 'rxjs' import { inspect } from 'util' -import { ErrorCodes, Message, StreamMessageReader as VSCodeStreamMessageReader, StreamMessageWriter as VSCodeStreamMessageWriter } from 'vscode-jsonrpc' -import { isNotificationMessage, isRequestMessage, isResponseMessage, NotificationMessage, RequestMessage, ResponseMessage } from 'vscode-jsonrpc/lib/messages' +import { + ErrorCodes, + Message, + StreamMessageReader as VSCodeStreamMessageReader, + StreamMessageWriter as VSCodeStreamMessageWriter, +} from 'vscode-jsonrpc' +import { + isNotificationMessage, + isRequestMessage, + isResponseMessage, + NotificationMessage, + RequestMessage, + ResponseMessage, +} from 'vscode-jsonrpc/lib/messages' import { Logger, NoopLogger } from './logging' import { InitializeParams, PartialResultParams } from './request-type' import { TypeScriptService } from './typescript-service' @@ -21,7 +33,12 @@ export interface HasMeta { * Returns true if the passed argument has a meta field */ function hasMeta(candidate: any): candidate is HasMeta { - return typeof candidate === 'object' && candidate !== null && typeof candidate.meta === 'object' && candidate.meta !== null + return ( + typeof candidate === 'object' && + candidate !== null && + typeof candidate.meta === 'object' && + candidate.meta !== null + ) } /** @@ -51,7 +68,6 @@ export interface MessageLogOptions { * In opposite to StreamMessageReader, supports multiple listeners and is compatible with Observables */ export class MessageEmitter extends EventEmitter { - constructor(input: NodeJS.ReadableStream, options: MessageLogOptions = {}) { super() const reader = new VSCodeStreamMessageReader(input) @@ -104,7 +120,6 @@ export class MessageEmitter extends EventEmitter { * consistent event API */ export class MessageWriter { - private logger: Logger private logMessages: boolean private vscodeWriter: VSCodeStreamMessageWriter @@ -134,7 +149,6 @@ export class MessageWriter { } export interface RegisterLanguageHandlerOptions { - logger?: Logger /** An opentracing-compatible tracer */ @@ -154,7 +168,6 @@ export function registerLanguageHandler( handler: TypeScriptService, options: RegisterLanguageHandlerOptions = {} ): void { - const logger = options.logger || new NoopLogger() const tracer = options.tracer || new Tracer() @@ -282,30 +295,33 @@ export function registerLanguageHandler( subscriptions.delete(message.id) }) }) - .subscribe(result => { - // Send final result - messageWriter.write({ - jsonrpc: '2.0', - id: message.id, - result, - }) - }, err => { - // Set error on span - span.setTag('error', true) - span.log({ 'event': 'error', 'error.object': err, 'message': err.message, 'stack': err.stack }) - // Log error - logger.error(`Handler for ${message.method} failed:`, err, '\nMessage:', message) - // Send error response - messageWriter.write({ - jsonrpc: '2.0', - id: message.id, - error: { - message: err.message + '', - code: typeof err.code === 'number' ? err.code : ErrorCodes.UnknownErrorCode, - data: omit(err, ['message', 'code']), - }, - }) - }) + .subscribe( + result => { + // Send final result + messageWriter.write({ + jsonrpc: '2.0', + id: message.id, + result, + }) + }, + err => { + // Set error on span + span.setTag('error', true) + span.log({ event: 'error', 'error.object': err, message: err.message, stack: err.stack }) + // Log error + logger.error(`Handler for ${message.method} failed:`, err, '\nMessage:', message) + // Send error response + messageWriter.write({ + jsonrpc: '2.0', + id: message.id, + error: { + message: err.message + '', + code: typeof err.code === 'number' ? err.code : ErrorCodes.UnknownErrorCode, + data: omit(err, ['message', 'code']), + }, + }) + } + ) // Save subscription for $/cancelRequest subscriptions.set(message.id, subscription) } else { diff --git a/src/diagnostics.ts b/src/diagnostics.ts index 74f446739..1a7a62d80 100644 --- a/src/diagnostics.ts +++ b/src/diagnostics.ts @@ -6,7 +6,7 @@ import { Diagnostic, DiagnosticSeverity, Range } from 'vscode-languageserver' */ export function convertTsDiagnostic(diagnostic: ts.Diagnostic): Diagnostic { const text = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n') - let range: Range = { start: {character: 0, line: 0}, end: {character: 0, line: 0} } + let range: Range = { start: { character: 0, line: 0 }, end: { character: 0, line: 0 } } if (diagnostic.file && diagnostic.start && diagnostic.length) { range = { start: diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start), @@ -35,6 +35,6 @@ function convertDiagnosticCategory(category: ts.DiagnosticCategory): DiagnosticS return DiagnosticSeverity.Warning case ts.DiagnosticCategory.Message: return DiagnosticSeverity.Information - // unmapped: DiagnosticSeverity.Hint + // unmapped: DiagnosticSeverity.Hint } } diff --git a/src/disposable.ts b/src/disposable.ts index 0e7bed7c3..b52b810e7 100644 --- a/src/disposable.ts +++ b/src/disposable.ts @@ -1,4 +1,3 @@ - /** * Interface for objects to perform actions if they are not needed anymore */ diff --git a/src/fs.ts b/src/fs.ts index 09554b5ed..924900488 100644 --- a/src/fs.ts +++ b/src/fs.ts @@ -27,7 +27,6 @@ export interface FileSystem { } export class RemoteFileSystem implements FileSystem { - constructor(private client: LanguageClient) {} /** @@ -35,7 +34,8 @@ export class RemoteFileSystem implements FileSystem { * A language server can use the result to index files by filtering and doing a content request for each text document of interest. */ public getWorkspaceFiles(base?: string, childOf = new Span()): Observable { - return this.client.workspaceXfiles({ base }, childOf) + return this.client + .workspaceXfiles({ base }, childOf) .mergeMap(textDocuments => textDocuments) .map(textDocument => normalizeUri(textDocument.uri)) } @@ -46,13 +46,13 @@ export class RemoteFileSystem implements FileSystem { * directly. */ public getTextDocumentContent(uri: string, childOf = new Span()): Observable { - return this.client.textDocumentXcontent({ textDocument: { uri } }, childOf) + return this.client + .textDocumentXcontent({ textDocument: { uri } }, childOf) .map(textDocument => textDocument.text) } } export class LocalFileSystem implements FileSystem { - /** * @param rootUri The root URI that is used if `base` is not specified */ @@ -104,7 +104,6 @@ export class LocalFileSystem implements FileSystem { * TODO: Implement Disposable with Disposer */ export class FileSystemUpdater { - /** * Observable for a pending or completed structure fetch */ @@ -133,12 +132,15 @@ export class FileSystemUpdater { // Limit concurrent fetches const observable = Observable.fromPromise(this.concurrencyLimit.wait()) .mergeMap(() => this.remoteFs.getTextDocumentContent(uri)) - .do(content => { - this.concurrencyLimit.signal() - this.inMemoryFs.add(uri, content) - }, err => { - this.fetches.delete(uri) - }) + .do( + content => { + this.concurrencyLimit.signal() + this.inMemoryFs.add(uri, content) + }, + err => { + this.fetches.delete(uri) + } + ) .ignoreElements() .publishReplay() .refCount() as Observable @@ -167,16 +169,23 @@ export class FileSystemUpdater { * @param childOf A parent span for tracing */ public fetchStructure(childOf = new Span()): Observable { - const observable = traceObservable('Fetch workspace structure', childOf, span => - this.remoteFs.getWorkspaceFiles(undefined, span) - .do(uri => { - this.inMemoryFs.add(uri) - }, err => { - this.structureFetch = undefined - }) - .ignoreElements() - .publishReplay() - .refCount() as Observable + const observable = traceObservable( + 'Fetch workspace structure', + childOf, + span => + this.remoteFs + .getWorkspaceFiles(undefined, span) + .do( + uri => { + this.inMemoryFs.add(uri) + }, + err => { + this.structureFetch = undefined + } + ) + .ignoreElements() + .publishReplay() + .refCount() as Observable ) this.structureFetch = observable return observable @@ -189,8 +198,7 @@ export class FileSystemUpdater { * @param span An OpenTracing span for tracing */ public ensureStructure(childOf = new Span()): Observable { - return traceObservable('Ensure structure', childOf, span => - this.structureFetch || this.fetchStructure(span)) + return traceObservable('Ensure structure', childOf, span => this.structureFetch || this.fetchStructure(span)) } /** diff --git a/src/lang-handler.ts b/src/lang-handler.ts index 6cce09f67..c4a00c244 100644 --- a/src/lang-handler.ts +++ b/src/lang-handler.ts @@ -1,7 +1,13 @@ import { FORMAT_TEXT_MAP, Span } from 'opentracing' import { Observable } from 'rxjs' import { inspect } from 'util' -import { isResponseMessage, Message, NotificationMessage, RequestMessage, ResponseMessage } from 'vscode-jsonrpc/lib/messages' +import { + isResponseMessage, + Message, + NotificationMessage, + RequestMessage, + ResponseMessage, +} from 'vscode-jsonrpc/lib/messages' import { ApplyWorkspaceEditParams, ApplyWorkspaceEditResponse, @@ -12,12 +18,7 @@ import { } from 'vscode-languageserver' import { HasMeta } from './connection' import { MessageEmitter, MessageWriter } from './connection' -import { - CacheGetParams, - CacheSetParams, - TextDocumentContentParams, - WorkspaceFilesParams, -} from './request-type' +import { CacheGetParams, CacheSetParams, TextDocumentContentParams, WorkspaceFilesParams } from './request-type' import { traceObservable } from './tracing' export interface LanguageClient { @@ -73,7 +74,6 @@ export interface LanguageClient { * Methods are named after the camelCase version of the LSP method name */ export class RemoteLanguageClient { - /** The next request ID to use */ private idCounter = 1 @@ -197,7 +197,10 @@ export class RemoteLanguageClient { * * @param params The edits to apply. */ - public workspaceApplyEdit(params: ApplyWorkspaceEditParams, childOf = new Span()): Observable { + public workspaceApplyEdit( + params: ApplyWorkspaceEditParams, + childOf = new Span() + ): Observable { return this.request('workspace/applyEdit', params, childOf) } } diff --git a/src/language-server-stdio.ts b/src/language-server-stdio.ts index 66f97e9e6..dfe027b28 100644 --- a/src/language-server-stdio.ts +++ b/src/language-server-stdio.ts @@ -2,7 +2,13 @@ import { Tracer } from 'opentracing' import { isNotificationMessage } from 'vscode-jsonrpc/lib/messages' -import { MessageEmitter, MessageLogOptions, MessageWriter, registerLanguageHandler, RegisterLanguageHandlerOptions } from './connection' +import { + MessageEmitter, + MessageLogOptions, + MessageWriter, + registerLanguageHandler, + RegisterLanguageHandlerOptions, +} from './connection' import { RemoteLanguageClient } from './lang-handler' import { FileLogger, StderrLogger } from './logging' import { TypeScriptService, TypeScriptServiceOptions } from './typescript-service' @@ -20,7 +26,9 @@ program .parse(process.argv) const logger = program.logfile ? new FileLogger(program.logfile) : new StderrLogger() -const tracer = program.enableJaeger ? initTracer({ serviceName: 'javascript-typescript-langserver', sampler: { type: 'const', param: 1 } }) : new Tracer() +const tracer = program.enableJaeger + ? initTracer({ serviceName: 'javascript-typescript-langserver', sampler: { type: 'const', param: 1 } }) + : new Tracer() const options: TypeScriptServiceOptions & MessageLogOptions & RegisterLanguageHandlerOptions = { strict: program.strict, diff --git a/src/language-server.ts b/src/language-server.ts index 1a3ad7721..8e374c680 100644 --- a/src/language-server.ts +++ b/src/language-server.ts @@ -15,7 +15,11 @@ program .version(packageJson.version) .option('-s, --strict', 'enabled strict mode') .option('-p, --port [port]', 'specifies LSP port to use (' + defaultLspPort + ')', parseInt) - .option('-c, --cluster [num]', 'number of concurrent cluster workers (defaults to number of CPUs, ' + numCPUs + ')', parseInt) + .option( + '-c, --cluster [num]', + 'number of concurrent cluster workers (defaults to number of CPUs, ' + numCPUs + ')', + parseInt + ) .option('-t, --trace', 'print all requests and responses') .option('-l, --logfile [file]', 'log to this file') .option('-j, --enable-jaeger', 'enable OpenTracing through Jaeger') @@ -27,7 +31,9 @@ const options: ServeOptions & TypeScriptServiceOptions = { strict: program.strict, logMessages: program.trace, logger: program.logfile ? new FileLogger(program.logfile) : new StdioLogger(), - tracer: program.enableJaeger ? initTracer({ serviceName: 'javascript-typescript-langserver', sampler: { type: 'const', param: 1 } }) : new Tracer(), + tracer: program.enableJaeger + ? initTracer({ serviceName: 'javascript-typescript-langserver', sampler: { type: 'const', param: 1 } }) + : new Tracer(), } serve(options, client => new TypeScriptService(client, options)) diff --git a/src/logging.ts b/src/logging.ts index b3661c6c6..fd899e650 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -1,4 +1,3 @@ - import chalk from 'chalk' import * as fs from 'fs' import { inspect } from 'util' @@ -16,14 +15,13 @@ export interface Logger { * Formats values to a message by pretty-printing objects */ function format(values: any[]): string { - return values.map(value => typeof value === 'string' ? value : inspect(value, {depth: Infinity})).join(' ') + return values.map(value => (typeof value === 'string' ? value : inspect(value, { depth: Infinity }))).join(' ') } /** * A logger implementation that sends window/logMessage notifications to an LSP client */ export class LSPLogger implements Logger { - /** * @param client The client to send window/logMessage notifications to */ @@ -135,7 +133,6 @@ export class FileLogger extends StreamLogger { * Logger implementation that wraps another logger and prefixes every message with a given prefix */ export class PrefixedLogger { - constructor(private logger: Logger, private prefix: string) {} public log(...values: any[]): void { diff --git a/src/match-files.ts b/src/match-files.ts index ba41295d8..e2b05d840 100644 --- a/src/match-files.ts +++ b/src/match-files.ts @@ -5,157 +5,186 @@ /* tslint:disable */ export interface FileSystemEntries { - files: string[]; - directories: string[]; + files: string[] + directories: string[] } -export function matchFiles(path: string, extensions: string[], excludes: string[], includes: string[], useCaseSensitiveFileNames: boolean, currentDirectory: string, getFileSystemEntries: (path: string) => FileSystemEntries): string[] { - - path = normalizePath(path); - currentDirectory = normalizePath(currentDirectory); - - const patterns = getFileMatcherPatterns(path, extensions, excludes, includes, useCaseSensitiveFileNames, currentDirectory); - - const regexFlag = useCaseSensitiveFileNames ? '' : 'i'; - - const includeFileRegex = patterns.includeFilePattern && new RegExp(patterns.includeFilePattern, regexFlag); - - const includeDirectoryRegex = patterns.includeDirectoryPattern && new RegExp(patterns.includeDirectoryPattern, regexFlag); - const excludeRegex = patterns.excludePattern && new RegExp(patterns.excludePattern, regexFlag); - - const result: string[] = []; +export function matchFiles( + path: string, + extensions: string[], + excludes: string[], + includes: string[], + useCaseSensitiveFileNames: boolean, + currentDirectory: string, + getFileSystemEntries: (path: string) => FileSystemEntries +): string[] { + path = normalizePath(path) + currentDirectory = normalizePath(currentDirectory) + + const patterns = getFileMatcherPatterns( + path, + extensions, + excludes, + includes, + useCaseSensitiveFileNames, + currentDirectory + ) + + const regexFlag = useCaseSensitiveFileNames ? '' : 'i' + + const includeFileRegex = patterns.includeFilePattern && new RegExp(patterns.includeFilePattern, regexFlag) + + const includeDirectoryRegex = + patterns.includeDirectoryPattern && new RegExp(patterns.includeDirectoryPattern, regexFlag) + const excludeRegex = patterns.excludePattern && new RegExp(patterns.excludePattern, regexFlag) + + const result: string[] = [] for (const basePath of patterns.basePaths) { - visitDirectory(basePath, combinePaths(currentDirectory, basePath)); + visitDirectory(basePath, combinePaths(currentDirectory, basePath)) } - return result; + return result function visitDirectory(path: string, absolutePath: string) { - const { files, directories } = getFileSystemEntries(path); + const { files, directories } = getFileSystemEntries(path) for (const current of files) { - const name = combinePaths(path, current); - const absoluteName = combinePaths(absolutePath, current); - if ((!extensions || fileExtensionIsAny(name, extensions)) && + const name = combinePaths(path, current) + const absoluteName = combinePaths(absolutePath, current) + if ( + (!extensions || fileExtensionIsAny(name, extensions)) && (!includeFileRegex || includeFileRegex.test(absoluteName)) && - (!excludeRegex || !excludeRegex.test(absoluteName))) { - result.push(name); + (!excludeRegex || !excludeRegex.test(absoluteName)) + ) { + result.push(name) } } for (const current of directories) { - const name = combinePaths(path, current); - const absoluteName = combinePaths(absolutePath, current); - if ((!includeDirectoryRegex || includeDirectoryRegex.test(absoluteName)) && - (!excludeRegex || !excludeRegex.test(absoluteName))) { - visitDirectory(name, absoluteName); + const name = combinePaths(path, current) + const absoluteName = combinePaths(absolutePath, current) + if ( + (!includeDirectoryRegex || includeDirectoryRegex.test(absoluteName)) && + (!excludeRegex || !excludeRegex.test(absoluteName)) + ) { + visitDirectory(name, absoluteName) } } } } -const directorySeparator = '/'; +const directorySeparator = '/' export function combinePaths(path1: string, path2: string) { - if (!(path1 && path1.length)) return path2; - if (!(path2 && path2.length)) return path1; - if (getRootLength(path2) !== 0) return path2; - if (path1.charAt(path1.length - 1) === directorySeparator) return path1 + path2; - return path1 + directorySeparator + path2; + if (!(path1 && path1.length)) return path2 + if (!(path2 && path2.length)) return path1 + if (getRootLength(path2) !== 0) return path2 + if (path1.charAt(path1.length - 1) === directorySeparator) return path1 + path2 + return path1 + directorySeparator + path2 } function normalizePath(path: string): string { - path = normalizeSlashes(path); - const rootLength = getRootLength(path); - const root = path.substr(0, rootLength); - const normalized = getNormalizedParts(path, rootLength); + path = normalizeSlashes(path) + const rootLength = getRootLength(path) + const root = path.substr(0, rootLength) + const normalized = getNormalizedParts(path, rootLength) if (normalized.length) { - const joinedParts = root + normalized.join(directorySeparator); - return pathEndsWithDirectorySeparator(path) ? joinedParts + directorySeparator : joinedParts; - } - else { - return root; + const joinedParts = root + normalized.join(directorySeparator) + return pathEndsWithDirectorySeparator(path) ? joinedParts + directorySeparator : joinedParts + } else { + return root } } -function getFileMatcherPatterns(path: string, extensions: string[], excludes: string[], includes: string[], useCaseSensitiveFileNames: boolean, currentDirectory: string): FileMatcherPatterns { - path = normalizePath(path); - currentDirectory = normalizePath(currentDirectory); - const absolutePath = combinePaths(currentDirectory, path); +function getFileMatcherPatterns( + path: string, + extensions: string[], + excludes: string[], + includes: string[], + useCaseSensitiveFileNames: boolean, + currentDirectory: string +): FileMatcherPatterns { + path = normalizePath(path) + currentDirectory = normalizePath(currentDirectory) + const absolutePath = combinePaths(currentDirectory, path) return { includeFilePattern: getRegularExpressionForWildcard(includes, absolutePath, 'files') || '', includeDirectoryPattern: getRegularExpressionForWildcard(includes, absolutePath, 'directories') || '', excludePattern: getRegularExpressionForWildcard(excludes, absolutePath, 'exclude') || '', basePaths: getBasePaths(path, includes, useCaseSensitiveFileNames) || [], - }; + } } function fileExtensionIs(path: string, extension: string): boolean { - return path.length > extension.length && endsWith(path, extension); + return path.length > extension.length && endsWith(path, extension) } function fileExtensionIsAny(path: string, extensions: string[]): boolean { for (const extension of extensions) { if (fileExtensionIs(path, extension)) { - return true; + return true } } - return false; + return false } -function getRegularExpressionForWildcard(specs: string[], basePath: string, usage: 'files' | 'directories' | 'exclude') { +function getRegularExpressionForWildcard( + specs: string[], + basePath: string, + usage: 'files' | 'directories' | 'exclude' +) { if (specs === undefined || specs.length === 0) { - return undefined; + return undefined } - const replaceWildcardCharacter = usage === 'files' ? replaceWildCardCharacterFiles : replaceWildCardCharacterOther; - const singleAsteriskRegexFragment = usage === 'files' ? singleAsteriskRegexFragmentFiles : singleAsteriskRegexFragmentOther; + const replaceWildcardCharacter = usage === 'files' ? replaceWildCardCharacterFiles : replaceWildCardCharacterOther + const singleAsteriskRegexFragment = + usage === 'files' ? singleAsteriskRegexFragmentFiles : singleAsteriskRegexFragmentOther /** * Regex for the ** wildcard. Matches any number of subdirectories. When used for including * files or directories, does not match subdirectories that start with a . character */ - const doubleAsteriskRegexFragment = usage === 'exclude' ? '(/.+?)?' : '(/[^/.][^/]*)*?'; + const doubleAsteriskRegexFragment = usage === 'exclude' ? '(/.+?)?' : '(/[^/.][^/]*)*?' - let pattern = ''; - let hasWrittenSubpattern = false; + let pattern = '' + let hasWrittenSubpattern = false spec: for (const spec of specs) { if (!spec) { - continue; + continue } - let subpattern = ''; - let hasRecursiveDirectoryWildcard = false; - let hasWrittenComponent = false; - const components = getNormalizedPathComponents(spec, basePath); + let subpattern = '' + let hasRecursiveDirectoryWildcard = false + let hasWrittenComponent = false + const components = getNormalizedPathComponents(spec, basePath) if (usage !== 'exclude' && components[components.length - 1] === '**') { - continue spec; + continue spec } // getNormalizedPathComponents includes the separator for the root component. // We need to remove to create our regex correctly. - components[0] = removeTrailingDirectorySeparator(components[0]); + components[0] = removeTrailingDirectorySeparator(components[0]) - let optionalCount = 0; + let optionalCount = 0 for (let component of components) { if (component === '**') { if (hasRecursiveDirectoryWildcard) { - continue spec; + continue spec } - subpattern += doubleAsteriskRegexFragment; - hasRecursiveDirectoryWildcard = true; - hasWrittenComponent = true; - } - else { + subpattern += doubleAsteriskRegexFragment + hasRecursiveDirectoryWildcard = true + hasWrittenComponent = true + } else { if (usage === 'directories') { - subpattern += '('; - optionalCount++; + subpattern += '(' + optionalCount++ } if (hasWrittenComponent) { - subpattern += directorySeparator; + subpattern += directorySeparator } if (usage !== 'exclude') { @@ -163,52 +192,51 @@ function getRegularExpressionForWildcard(specs: string[], basePath: string, usag // appear first in a component. Dotted directories and files can be included explicitly // like so: **/.*/.* if (component.charCodeAt(0) === CharacterCodes.asterisk) { - subpattern += '([^./]' + singleAsteriskRegexFragment + ')?'; - component = component.substr(1); - } - else if (component.charCodeAt(0) === CharacterCodes.question) { - subpattern += '[^./]'; - component = component.substr(1); + subpattern += '([^./]' + singleAsteriskRegexFragment + ')?' + component = component.substr(1) + } else if (component.charCodeAt(0) === CharacterCodes.question) { + subpattern += '[^./]' + component = component.substr(1) } } - subpattern += component.replace(reservedCharacterPattern, replaceWildcardCharacter); - hasWrittenComponent = true; + subpattern += component.replace(reservedCharacterPattern, replaceWildcardCharacter) + hasWrittenComponent = true } } while (optionalCount > 0) { - subpattern += ')?'; - optionalCount--; + subpattern += ')?' + optionalCount-- } if (hasWrittenSubpattern) { - pattern += '|'; + pattern += '|' } - pattern += '(' + subpattern + ')'; - hasWrittenSubpattern = true; + pattern += '(' + subpattern + ')' + hasWrittenSubpattern = true } if (!pattern) { - return undefined; + return undefined } - return '^(' + pattern + (usage === 'exclude' ? ')($|/)' : ')$'); + return '^(' + pattern + (usage === 'exclude' ? ')($|/)' : ')$') } function getRootLength(path: string): number { if (path.charCodeAt(0) === CharacterCodes.slash) { - if (path.charCodeAt(1) !== CharacterCodes.slash) return 1; - const p1 = path.indexOf('/', 2); - if (p1 < 0) return 2; - const p2 = path.indexOf('/', p1 + 1); - if (p2 < 0) return p1 + 1; - return p2 + 1; + if (path.charCodeAt(1) !== CharacterCodes.slash) return 1 + const p1 = path.indexOf('/', 2) + if (p1 < 0) return 2 + const p2 = path.indexOf('/', p1 + 1) + if (p2 < 0) return p1 + 1 + return p2 + 1 } if (path.charCodeAt(1) === CharacterCodes.colon) { - if (path.charCodeAt(2) === CharacterCodes.slash) return 3; - return 2; + if (path.charCodeAt(2) === CharacterCodes.slash) return 3 + return 2 } // Per RFC 1738 'file' URI schema has the shape file:/// // if is omitted then it is assumed that host value is 'localhost', @@ -216,180 +244,178 @@ function getRootLength(path: string): number { // file:///folder1/file1 - this is a correct URI // file://folder2/file2 - this is an incorrect URI if (path.lastIndexOf('file:///', 0) === 0) { - return 'file:///'.length; + return 'file:///'.length } - const idx = path.indexOf('://'); + const idx = path.indexOf('://') if (idx !== -1) { - return idx + '://'.length; + return idx + '://'.length } - return 0; + return 0 } function getNormalizedParts(normalizedSlashedPath: string, rootLength: number): string[] { - const parts = normalizedSlashedPath.substr(rootLength).split(directorySeparator); - const normalized: string[] = []; + const parts = normalizedSlashedPath.substr(rootLength).split(directorySeparator) + const normalized: string[] = [] for (const part of parts) { if (part !== '.') { if (part === '..' && normalized.length > 0 && lastOrUndefined(normalized) !== '..') { - normalized.pop(); - } - else { + normalized.pop() + } else { // A part may be an empty string (which is 'falsy') if the path had consecutive slashes, // e.g. "path//file.ts". Drop these before re-joining the parts. if (part) { - normalized.push(part); + normalized.push(part) } } } } - return normalized; + return normalized } function pathEndsWithDirectorySeparator(path: string): boolean { - return path.charCodeAt(path.length - 1) === directorySeparatorCharCode; + return path.charCodeAt(path.length - 1) === directorySeparatorCharCode } function replaceWildCardCharacterFiles(match: string) { - return replaceWildcardCharacter(match, singleAsteriskRegexFragmentFiles); + return replaceWildcardCharacter(match, singleAsteriskRegexFragmentFiles) } function replaceWildCardCharacterOther(match: string) { - return replaceWildcardCharacter(match, singleAsteriskRegexFragmentOther); + return replaceWildcardCharacter(match, singleAsteriskRegexFragmentOther) } function replaceWildcardCharacter(match: string, singleAsteriskRegexFragment: string) { - return match === '*' ? singleAsteriskRegexFragment : match === '?' ? '[^/]' : '\\' + match; + return match === '*' ? singleAsteriskRegexFragment : match === '?' ? '[^/]' : '\\' + match } function getBasePaths(path: string, includes: string[], useCaseSensitiveFileNames: boolean) { // Storage for our results in the form of literal paths (e.g. the paths as written by the user). - const basePaths: string[] = [path]; + const basePaths: string[] = [path] if (includes) { // Storage for literal base paths amongst the include patterns. - const includeBasePaths: string[] = []; + const includeBasePaths: string[] = [] for (const include of includes) { // We also need to check the relative paths by converting them to absolute and normalizing // in case they escape the base path (e.g "..\somedirectory") - const absolute: string = isRootedDiskPath(include) ? include : normalizePath(combinePaths(path, include)); + const absolute: string = isRootedDiskPath(include) ? include : normalizePath(combinePaths(path, include)) - const wildcardOffset = indexOfAnyCharCode(absolute, wildcardCharCodes); - const includeBasePath = wildcardOffset < 0 - ? removeTrailingDirectorySeparator(getDirectoryPath(absolute)) - : absolute.substring(0, absolute.lastIndexOf(directorySeparator, wildcardOffset)); + const wildcardOffset = indexOfAnyCharCode(absolute, wildcardCharCodes) + const includeBasePath = + wildcardOffset < 0 + ? removeTrailingDirectorySeparator(getDirectoryPath(absolute)) + : absolute.substring(0, absolute.lastIndexOf(directorySeparator, wildcardOffset)) // Append the literal and canonical candidate base paths. - includeBasePaths.push(includeBasePath); + includeBasePaths.push(includeBasePath) } // Sort the offsets array using either the literal or canonical path representations. - includeBasePaths.sort(useCaseSensitiveFileNames ? compareStrings : compareStringsCaseInsensitive); + includeBasePaths.sort(useCaseSensitiveFileNames ? compareStrings : compareStringsCaseInsensitive) // Iterate over each include base path and include unique base paths that are not a // subpath of an existing base path include: for (let i = 0; i < includeBasePaths.length; i++) { - const includeBasePath = includeBasePaths[i]; + const includeBasePath = includeBasePaths[i] for (let j = 0; j < basePaths.length; j++) { if (containsPath(basePaths[j], includeBasePath, path, !useCaseSensitiveFileNames)) { - continue include; + continue include } } - basePaths.push(includeBasePath); + basePaths.push(includeBasePath) } } - return basePaths; + return basePaths } function endsWith(str: string, suffix: string): boolean { - const expectedPos = str.length - suffix.length; - return expectedPos >= 0 && str.indexOf(suffix, expectedPos) === expectedPos; + const expectedPos = str.length - suffix.length + return expectedPos >= 0 && str.indexOf(suffix, expectedPos) === expectedPos } function compareStrings(a: string, b: string, ignoreCase?: boolean): Comparison { - if (a === b) return Comparison.EqualTo; - if (a === undefined) return Comparison.LessThan; - if (b === undefined) return Comparison.GreaterThan; + if (a === b) return Comparison.EqualTo + if (a === undefined) return Comparison.LessThan + if (b === undefined) return Comparison.GreaterThan if (ignoreCase) { if (String.prototype.localeCompare) { - const result = a.localeCompare(b, /*locales*/ undefined, { usage: 'sort', sensitivity: 'accent' }); - return result < 0 ? Comparison.LessThan : result > 0 ? Comparison.GreaterThan : Comparison.EqualTo; + const result = a.localeCompare(b, /*locales*/ undefined, { usage: 'sort', sensitivity: 'accent' }) + return result < 0 ? Comparison.LessThan : result > 0 ? Comparison.GreaterThan : Comparison.EqualTo } - a = a.toUpperCase(); - b = b.toUpperCase(); - if (a === b) return Comparison.EqualTo; + a = a.toUpperCase() + b = b.toUpperCase() + if (a === b) return Comparison.EqualTo } - return a < b ? Comparison.LessThan : Comparison.GreaterThan; + return a < b ? Comparison.LessThan : Comparison.GreaterThan } function compareStringsCaseInsensitive(a: string, b: string) { - return compareStrings(a, b, /*ignoreCase*/ true); + return compareStrings(a, b, /*ignoreCase*/ true) } -const singleAsteriskRegexFragmentFiles = '([^./]|(\\.(?!min\\.js$))?)*'; -const singleAsteriskRegexFragmentOther = '[^/]*'; +const singleAsteriskRegexFragmentFiles = '([^./]|(\\.(?!min\\.js$))?)*' +const singleAsteriskRegexFragmentOther = '[^/]*' function getNormalizedPathComponents(path: string, currentDirectory: string) { - path = normalizeSlashes(path); - let rootLength = getRootLength(path); + path = normalizeSlashes(path) + let rootLength = getRootLength(path) if (rootLength === 0) { // If the path is not rooted it is relative to current directory - path = combinePaths(normalizeSlashes(currentDirectory), path); - rootLength = getRootLength(path); + path = combinePaths(normalizeSlashes(currentDirectory), path) + rootLength = getRootLength(path) } - return normalizedPathComponents(path, rootLength); + return normalizedPathComponents(path, rootLength) } function normalizedPathComponents(path: string, rootLength: number) { - const normalizedParts = getNormalizedParts(path, rootLength); - return [path.substr(0, rootLength)].concat(normalizedParts); + const normalizedParts = getNormalizedParts(path, rootLength) + return [path.substr(0, rootLength)].concat(normalizedParts) } function containsPath(parent: string, child: string, currentDirectory: string, ignoreCase?: boolean) { - if (parent === undefined || child === undefined) return false; - if (parent === child) return true; - parent = removeTrailingDirectorySeparator(parent); - child = removeTrailingDirectorySeparator(child); - if (parent === child) return true; - const parentComponents = getNormalizedPathComponents(parent, currentDirectory); - const childComponents = getNormalizedPathComponents(child, currentDirectory); + if (parent === undefined || child === undefined) return false + if (parent === child) return true + parent = removeTrailingDirectorySeparator(parent) + child = removeTrailingDirectorySeparator(child) + if (parent === child) return true + const parentComponents = getNormalizedPathComponents(parent, currentDirectory) + const childComponents = getNormalizedPathComponents(child, currentDirectory) if (childComponents.length < parentComponents.length) { - return false; + return false } for (let i = 0; i < parentComponents.length; i++) { - const result = compareStrings(parentComponents[i], childComponents[i], ignoreCase); + const result = compareStrings(parentComponents[i], childComponents[i], ignoreCase) if (result !== Comparison.EqualTo) { - return false; + return false } } - return true; + return true } function removeTrailingDirectorySeparator(path: string) { if (path.charAt(path.length - 1) === directorySeparator) { - return path.substr(0, path.length - 1); + return path.substr(0, path.length - 1) } - return path; + return path } function lastOrUndefined(array: T[]): T | void { - return array && array.length > 0 - ? array[array.length - 1] - : undefined; + return array && array.length > 0 ? array[array.length - 1] : undefined } interface FileMatcherPatterns { - includeFilePattern: string; - includeDirectoryPattern: string; - excludePattern: string; - basePaths: string[]; + includeFilePattern: string + includeDirectoryPattern: string + excludePattern: string + basePaths: string[] } const enum Comparison { @@ -400,17 +426,17 @@ const enum Comparison { const enum CharacterCodes { nullCharacter = 0, - maxAsciiCharacter = 0x7F, + maxAsciiCharacter = 0x7f, - lineFeed = 0x0A, // \n - carriageReturn = 0x0D, // \r + lineFeed = 0x0a, // \n + carriageReturn = 0x0d, // \r lineSeparator = 0x2028, paragraphSeparator = 0x2029, nextLine = 0x0085, // Unicode 3.0 space characters - space = 0x0020, // " " - nonBreakingSpace = 0x00A0, // + space = 0x0020, // " " + nonBreakingSpace = 0x00a0, // enQuad = 0x2000, emQuad = 0x2001, enSpace = 0x2002, @@ -421,14 +447,14 @@ const enum CharacterCodes { figureSpace = 0x2007, punctuationSpace = 0x2008, thinSpace = 0x2009, - hairSpace = 0x200A, - zeroWidthSpace = 0x200B, - narrowNoBreakSpace = 0x202F, + hairSpace = 0x200a, + zeroWidthSpace = 0x200b, + narrowNoBreakSpace = 0x202f, ideographicSpace = 0x3000, - mathematicalSpace = 0x205F, + mathematicalSpace = 0x205f, ogham = 0x1680, - _ = 0x5F, + _ = 0x5f, $ = 0x24, _0 = 0x30, @@ -451,12 +477,12 @@ const enum CharacterCodes { g = 0x67, h = 0x68, i = 0x69, - j = 0x6A, - k = 0x6B, - l = 0x6C, - m = 0x6D, - n = 0x6E, - o = 0x6F, + j = 0x6a, + k = 0x6b, + l = 0x6c, + m = 0x6d, + n = 0x6e, + o = 0x6f, p = 0x70, q = 0x71, r = 0x72, @@ -467,7 +493,7 @@ const enum CharacterCodes { w = 0x77, x = 0x78, y = 0x79, - z = 0x7A, + z = 0x7a, A = 0x41, B = 0x42, @@ -478,12 +504,12 @@ const enum CharacterCodes { G = 0x47, H = 0x48, I = 0x49, - J = 0x4A, - K = 0x4B, - L = 0x4C, - M = 0x4D, - N = 0x4E, - O = 0x4F, + J = 0x4a, + K = 0x4b, + L = 0x4c, + M = 0x4d, + N = 0x4e, + O = 0x4f, P = 0x50, Q = 0x51, R = 0x52, @@ -496,78 +522,78 @@ const enum CharacterCodes { Y = 0x59, Z = 0x5a, - ampersand = 0x26, // & - asterisk = 0x2A, // * - at = 0x40, // @ - backslash = 0x5C, // \ - backtick = 0x60, // ` - bar = 0x7C, // | - caret = 0x5E, // ^ - closeBrace = 0x7D, // } - closeBracket = 0x5D, // ] - closeParen = 0x29, // ) - colon = 0x3A, // : - comma = 0x2C, // , - dot = 0x2E, // . - doubleQuote = 0x22, // " - equals = 0x3D, // = - exclamation = 0x21, // ! - greaterThan = 0x3E, // > - hash = 0x23, // # - lessThan = 0x3C, // < - minus = 0x2D, // - - openBrace = 0x7B, // { - openBracket = 0x5B, // [ - openParen = 0x28, // ( - percent = 0x25, // % - plus = 0x2B, // + - question = 0x3F, // ? - semicolon = 0x3B, // ; - singleQuote = 0x27, // ' - slash = 0x2F, // / - tilde = 0x7E, // ~ - - backspace = 0x08, // \b - formFeed = 0x0C, // \f - byteOrderMark = 0xFEFF, - tab = 0x09, // \t - verticalTab = 0x0B, // \v + ampersand = 0x26, // & + asterisk = 0x2a, // * + at = 0x40, // @ + backslash = 0x5c, // \ + backtick = 0x60, // ` + bar = 0x7c, // | + caret = 0x5e, // ^ + closeBrace = 0x7d, // } + closeBracket = 0x5d, // ] + closeParen = 0x29, // ) + colon = 0x3a, // : + comma = 0x2c, // , + dot = 0x2e, // . + doubleQuote = 0x22, // " + equals = 0x3d, // = + exclamation = 0x21, // ! + greaterThan = 0x3e, // > + hash = 0x23, // # + lessThan = 0x3c, // < + minus = 0x2d, // - + openBrace = 0x7b, // { + openBracket = 0x5b, // [ + openParen = 0x28, // ( + percent = 0x25, // % + plus = 0x2b, // + + question = 0x3f, // ? + semicolon = 0x3b, // ; + singleQuote = 0x27, // ' + slash = 0x2f, // / + tilde = 0x7e, // ~ + + backspace = 0x08, // \b + formFeed = 0x0c, // \f + byteOrderMark = 0xfeff, + tab = 0x09, // \t + verticalTab = 0x0b, // \v } -const reservedCharacterPattern = /[^\w\s\/]/g; +const reservedCharacterPattern = /[^\w\s\/]/g -const directorySeparatorCharCode = CharacterCodes.slash; +const directorySeparatorCharCode = CharacterCodes.slash function isRootedDiskPath(path: string) { - return getRootLength(path) !== 0; + return getRootLength(path) !== 0 } function indexOfAnyCharCode(text: string, charCodes: number[], start?: number): number { for (let i = start || 0, len = text.length; i < len; i++) { if (contains(charCodes, text.charCodeAt(i))) { - return i; + return i } } - return -1; + return -1 } -const wildcardCharCodes = [CharacterCodes.asterisk, CharacterCodes.question]; +const wildcardCharCodes = [CharacterCodes.asterisk, CharacterCodes.question] function getDirectoryPath(path: string): any { - return path.substr(0, Math.max(getRootLength(path), path.lastIndexOf(directorySeparator))); + return path.substr(0, Math.max(getRootLength(path), path.lastIndexOf(directorySeparator))) } function contains(array: T[], value: T): boolean { if (array) { for (const v of array) { if (v === value) { - return true; + return true } } } - return false; + return false } function normalizeSlashes(path: string): string { - return path.replace(/\\/g, '/'); + return path.replace(/\\/g, '/') } diff --git a/src/memfs.ts b/src/memfs.ts index 4fe7cb7e3..4beff32fd 100644 --- a/src/memfs.ts +++ b/src/memfs.ts @@ -23,7 +23,6 @@ export interface FileSystemNode { * In-memory file system, can be served as a ParseConfigHost (thus allowing listing files that belong to project based on tsconfig.json options) */ export class InMemoryFileSystem extends EventEmitter implements ts.ParseConfigHost, ts.ModuleResolutionHost { - /** * Contains a Map of all URIs that exist in the workspace, optionally with a content. * File contents for URIs in it do not neccessarily have to be fetched already. @@ -208,21 +207,14 @@ export class InMemoryFileSystem extends EventEmitter implements ts.ParseConfigHo * Called by TS service to scan virtual directory when TS service looks for source files that belong to a project */ public readDirectory(rootDir: string, extensions: string[], excludes: string[], includes: string[]): string[] { - return matchFiles(rootDir, - extensions, - excludes, - includes, - true, - this.path, - p => this.getFileSystemEntries(p) - ) + return matchFiles(rootDir, extensions, excludes, includes, true, this.path, p => this.getFileSystemEntries(p)) } /** * Called by TS service to scan virtual directory when TS service looks for source files that belong to a project */ public getFileSystemEntries(path: string): FileSystemEntries { - const ret: { files: string[], directories: string[] } = { files: [], directories: [] } + const ret: { files: string[]; directories: string[] } = { files: [], directories: [] } let node = this.rootNode const components = path.split('/').filter(c => c) if (components.length !== 1 || components[0]) { diff --git a/src/packages.ts b/src/packages.ts index 9a603bc7f..347447a99 100644 --- a/src/packages.ts +++ b/src/packages.ts @@ -1,4 +1,3 @@ - import { EventEmitter } from 'events' import { Span } from 'opentracing' import * as path from 'path' @@ -18,32 +17,24 @@ export interface PackageJson { name?: string version?: string typings?: string - repository?: string | { type: string, url: string } + repository?: string | { type: string; url: string } dependencies?: { - [packageName: string]: string; + [packageName: string]: string } devDependencies?: { - [packageName: string]: string; + [packageName: string]: string } peerDependencies?: { - [packageName: string]: string; + [packageName: string]: string } optionalDependencies?: { - [packageName: string]: string; + [packageName: string]: string } } export const DEPENDENCY_KEYS: ReadonlyArray< - 'dependencies' | - 'devDependencies' | - 'peerDependencies' | - 'optionalDependencies' -> = [ - 'dependencies', - 'devDependencies', - 'peerDependencies', - 'optionalDependencies', -] + 'dependencies' | 'devDependencies' | 'peerDependencies' | 'optionalDependencies' +> = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'] /** * Matches: @@ -81,7 +72,6 @@ export function extractDefinitelyTypedPackageName(uri: string): string | undefin } export class PackageManager extends EventEmitter implements Disposable { - /** * Map of package.json URIs _defined_ in the workspace to optional content. * Does not include package.jsons of dependencies. @@ -109,33 +99,38 @@ export class PackageManager extends EventEmitter implements Disposable { let rootPackageJsonLevel = Infinity // Find locations of package.jsons _not_ inside node_modules this.subscriptions.add( - Observable.fromEvent(this.inMemoryFileSystem, 'add', Array.of as SelectorMethodSignature<[string, string]>) - .subscribe(([uri, content]) => { - const parts = url.parse(uri) - if (!parts.pathname || !parts.pathname.endsWith('/package.json') || parts.pathname.includes('/node_modules/')) { - return - } - let parsed: PackageJson | undefined - if (content) { - try { - parsed = JSON.parse(content) - } catch (err) { - logger.error(`Error parsing package.json:`, err) - } - } - // Don't override existing content with undefined - if (parsed || !this.packages.get(uri)) { - this.packages.set(uri, parsed) - this.logger.log(`Found package ${uri}`) - this.emit('parsed', uri, parsed) - } - // If the current root package.json is further nested than this one, replace it - const level = parts.pathname.split('/').length - if (level < rootPackageJsonLevel) { - this.rootPackageJsonUri = uri - rootPackageJsonLevel = level + Observable.fromEvent(this.inMemoryFileSystem, 'add', Array.of as SelectorMethodSignature< + [string, string] + >).subscribe(([uri, content]) => { + const parts = url.parse(uri) + if ( + !parts.pathname || + !parts.pathname.endsWith('/package.json') || + parts.pathname.includes('/node_modules/') + ) { + return + } + let parsed: PackageJson | undefined + if (content) { + try { + parsed = JSON.parse(content) + } catch (err) { + logger.error(`Error parsing package.json:`, err) } - }) + } + // Don't override existing content with undefined + if (parsed || !this.packages.get(uri)) { + this.packages.set(uri, parsed) + this.logger.log(`Found package ${uri}`) + this.emit('parsed', uri, parsed) + } + // If the current root package.json is further nested than this one, replace it + const level = parts.pathname.split('/').length + if (level < rootPackageJsonLevel) { + this.rootPackageJsonUri = uri + rootPackageJsonLevel = level + } + }) ) } @@ -162,14 +157,15 @@ export class PackageManager extends EventEmitter implements Disposable { * @return Observable that emits a single PackageJson or never */ public getClosestPackageJson(uri: string, span = new Span()): Observable { - return this.updater.ensureStructure() - .concat(Observable.defer(() => { + return this.updater.ensureStructure().concat( + Observable.defer(() => { const packageJsonUri = this.getClosestPackageJsonUri(uri) if (!packageJsonUri) { return Observable.empty() } return this.getPackageJson(packageJsonUri, span) - })) + }) + ) } /** @@ -188,14 +184,15 @@ export class PackageManager extends EventEmitter implements Disposable { if (packageJson) { return Observable.of(packageJson) } - return this.updater.ensure(uri, span) - .concat(Observable.defer(() => { + return this.updater.ensure(uri, span).concat( + Observable.defer(() => { packageJson = this.packages.get(uri)! if (!packageJson) { return Observable.throw(new Error(`Expected ${uri} to be registered in PackageManager`)) } return Observable.of(packageJson) - })) + }) + ) }) } diff --git a/src/plugins.ts b/src/plugins.ts index 43498e09c..bed3e977e 100644 --- a/src/plugins.ts +++ b/src/plugins.ts @@ -41,7 +41,7 @@ export interface PluginCreateInfo { */ export interface Project { projectService: { - logger: Logger; + logger: Logger } } @@ -65,10 +65,9 @@ export type ServerHost = object /** * The result of a node require: a module or an error. */ -type RequireResult = { module: {}, error: undefined } | { module: undefined, error: {} } +type RequireResult = { module: {}; error: undefined } | { module: undefined; error: {} } export class PluginLoader { - private allowLocalPluginLoads = false private globalPlugins: string[] = [] private pluginProbeLocations: string[] = [] @@ -79,7 +78,8 @@ export class PluginLoader { pluginSettings?: PluginSettings, private logger = new NoopLogger(), private resolutionHost = new LocalModuleResolutionHost(), - private requireModule: (moduleName: string) => any = require) { + private requireModule: (moduleName: string) => any = require + ) { if (pluginSettings) { this.allowLocalPluginLoads = pluginSettings.allowLocalPluginLoads || false this.globalPlugins = pluginSettings.globalPlugins || [] @@ -130,9 +130,13 @@ export class PluginLoader { * @param pluginConfigEntry * @param searchPaths */ - private enablePlugin(pluginConfigEntry: ts.PluginImport, searchPaths: string[], enableProxy: EnableProxyFunc): void { + private enablePlugin( + pluginConfigEntry: ts.PluginImport, + searchPaths: string[], + enableProxy: EnableProxyFunc + ): void { for (const searchPath of searchPaths) { - const resolvedModule = this.resolveModule(pluginConfigEntry.name, searchPath) as PluginModuleFactory + const resolvedModule = this.resolveModule(pluginConfigEntry.name, searchPath) as PluginModuleFactory if (resolvedModule) { enableProxy(resolvedModule, pluginConfigEntry) return @@ -179,14 +183,13 @@ export class PluginLoader { */ private resolveJavaScriptModule(moduleName: string, initialDir: string, host: ts.ModuleResolutionHost): string { // TODO: this should set jsOnly=true to the internal resolver, but this parameter is not exposed on a public api. - const result = - ts.nodeModuleNameResolver( - moduleName, - initialDir.replace('\\', '/') + '/package.json', /* containingFile */ - { moduleResolution: ts.ModuleResolutionKind.NodeJs, allowJs: true }, - this.resolutionHost, - undefined - ) + const result = ts.nodeModuleNameResolver( + moduleName, + initialDir.replace('\\', '/') + '/package.json' /* containingFile */, + { moduleResolution: ts.ModuleResolutionKind.NodeJs, allowJs: true }, + this.resolutionHost, + undefined + ) if (!result.resolvedModule) { // this.logger.error(result.failedLookupLocations); throw new Error(`Could not resolve JS module ${moduleName} starting at ${initialDir}.`) diff --git a/src/project-manager.ts b/src/project-manager.ts index c6f462827..b910f8ee7 100644 --- a/src/project-manager.ts +++ b/src/project-manager.ts @@ -33,7 +33,6 @@ const LAST_FORWARD_OR_BACKWARD_SLASH = /[\\\/][^\\\/]*$/ * @implements ts.LanguageServiceHost */ export class InMemoryLanguageServiceHost implements ts.LanguageServiceHost { - public complete: boolean /** @@ -70,7 +69,13 @@ export class InMemoryLanguageServiceHost implements ts.LanguageServiceHost { */ private versions: Map - constructor(rootPath: string, options: ts.CompilerOptions, fs: InMemoryFileSystem, versions: Map, private logger: Logger = new NoopLogger()) { + constructor( + rootPath: string, + options: ts.CompilerOptions, + fs: InMemoryFileSystem, + versions: Map, + private logger: Logger = new NoopLogger() + ) { this.rootPath = rootPath this.options = options this.fs = fs @@ -191,7 +196,6 @@ export class InMemoryLanguageServiceHost implements ts.LanguageServiceHost { * expectedFilePaths and typeRoots) */ export class ProjectConfiguration { - private service?: ts.LanguageService /** @@ -349,9 +353,9 @@ export class ProjectConfiguration { const options = configParseResult.options const pathResolver = /^[a-z]:\//i.test(base) ? path.win32 : path.posix - this.typeRoots = options.typeRoots ? - options.typeRoots.map((r: string) => toUnixPath(pathResolver.resolve(this.rootFilePath, r))) : - [] + this.typeRoots = options.typeRoots + ? options.typeRoots.map((r: string) => toUnixPath(pathResolver.resolve(this.rootFilePath, r))) + : [] if (/(^|\/)jsconfig\.json$/.test(this.configFilePath)) { options.allowJs = true @@ -359,13 +363,7 @@ export class ProjectConfiguration { if (this.traceModuleResolution) { options.traceResolution = true } - this.host = new InMemoryLanguageServiceHost( - this.fs.path, - options, - this.fs, - this.versions, - this.logger - ) + this.host = new InMemoryLanguageServiceHost(this.fs.path, options, this.fs, this.versions, this.logger) this.service = ts.createLanguageService(this.host, this.documentRegistry) const pluginLoader = new PluginLoader(this.rootFilePath, this.fs, this.pluginSettings, this.logger) pluginLoader.loadPlugins(options, (factory, config) => this.wrapService(factory, config)) @@ -380,13 +378,15 @@ export class ProjectConfiguration { private wrapService(pluginModuleFactory: PluginModuleFactory, configEntry: ts.PluginImport): void { try { if (typeof pluginModuleFactory !== 'function') { - this.logger.info(`Skipped loading plugin ${configEntry.name} because it didn't expose a proper factory function`) + this.logger.info( + `Skipped loading plugin ${configEntry.name} because it didn't expose a proper factory function` + ) return } const info: PluginCreateInfo = { config: configEntry, - project: { projectService: { logger: this.logger }}, // TODO: may need more support + project: { projectService: { logger: this.logger } }, // TODO: may need more support languageService: this.getService(), languageServiceHost: this.getHost(), serverHost: {}, // TODO: may need an adapter @@ -411,9 +411,10 @@ export class ProjectConfiguration { * @param fileName A Unix-like absolute file path. */ public isExpectedDeclarationFile(fileName: string): boolean { - return isDeclarationFile(fileName) && - (this.expectedFilePaths.has(fileName) || - this.typeRoots.some(root => fileName.startsWith(root))) + return ( + isDeclarationFile(fileName) && + (this.expectedFilePaths.has(fileName) || this.typeRoots.some(root => fileName.startsWith(root))) + ) } /** @@ -435,8 +436,7 @@ export class ProjectConfiguration { for (const uri of this.fs.uris()) { const fileName = uri2path(uri) const unixPath = toUnixPath(fileName) - if (isGlobalTSFile(unixPath) || - this.isExpectedDeclarationFile(unixPath)) { + if (isGlobalTSFile(unixPath) || this.isExpectedDeclarationFile(unixPath)) { const sourceFile = program.getSourceFile(fileName) if (!sourceFile) { this.getHost().addFile(fileName) @@ -499,7 +499,6 @@ export type ConfigType = 'js' | 'ts' * ProjectManager preserves Windows paths until passed to ProjectConfiguration or TS APIs. */ export class ProjectManager implements Disposable { - /** * Root path with slashes */ @@ -599,7 +598,7 @@ export class ProjectManager implements Disposable { // Create catch-all fallback configs in case there are no tsconfig.json files // They are removed once at least one tsconfig.json is found const trimmedRootPath = this.rootPath.replace(/[\\\/]+$/, '') - const fallbackConfigs: {js?: ProjectConfiguration, ts?: ProjectConfiguration} = {} + const fallbackConfigs: { js?: ProjectConfiguration; ts?: ProjectConfiguration } = {} for (const configType of ['js', 'ts'] as ConfigType[]) { const configs = this.configs[configType] const tsConfig: any = { @@ -628,24 +627,29 @@ export class ProjectManager implements Disposable { // Whenever a file with content is added to the InMemoryFileSystem, check if it's a tsconfig.json and add a new ProjectConfiguration this.subscriptions.add( Observable.fromEvent(inMemoryFileSystem, 'add', Array.of as SelectorMethodSignature<[string, string]>) - .filter(([uri, content]) => !!content && /\/[tj]sconfig\.json/.test(uri) && !uri.includes('/node_modules/')) + .filter( + ([uri, content]) => !!content && /\/[tj]sconfig\.json/.test(uri) && !uri.includes('/node_modules/') + ) .subscribe(([uri, content]) => { const filePath = uri2path(uri) const pos = filePath.search(LAST_FORWARD_OR_BACKWARD_SLASH) const dir = pos <= 0 ? '' : filePath.substring(0, pos) const configType = this.getConfigurationType(filePath) const configs = this.configs[configType] - configs.set(dir, new ProjectConfiguration( - this.inMemoryFs, - documentRegistry, + configs.set( dir, - this.versions, - filePath, - undefined, - this.traceModuleResolution, - this.pluginSettings, - this.logger - )) + new ProjectConfiguration( + this.inMemoryFs, + documentRegistry, + dir, + this.versions, + filePath, + undefined, + this.traceModuleResolution, + this.pluginSettings, + this.logger + ) + ) // Remove catch-all config (if exists) if (configs.get(trimmedRootPath) === fallbackConfigs[configType]) { configs.delete(trimmedRootPath) @@ -701,22 +705,27 @@ export class ProjectManager implements Disposable { public ensureModuleStructure(childOf = new Span()): Observable { return traceObservable('Ensure module structure', childOf, span => { if (!this.ensuredModuleStructure) { - this.ensuredModuleStructure = this.updater.ensureStructure() + this.ensuredModuleStructure = this.updater + .ensureStructure() // Ensure content of all all global .d.ts, [tj]sconfig.json, package.json files .concat(Observable.defer(() => observableFromIterable(this.inMemoryFs.uris()))) .filter(uri => isGlobalTSFile(uri) || isConfigFile(uri) || isPackageJsonFile(uri)) .mergeMap(uri => this.updater.ensure(uri)) - .do(noop, err => { - this.ensuredModuleStructure = undefined - }, () => { - // Reset all compilation state - // TODO ze incremental compilation instead - for (const config of this.configurations()) { - config.reset() + .do( + noop, + err => { + this.ensuredModuleStructure = undefined + }, + () => { + // Reset all compilation state + // TODO ze incremental compilation instead + for (const config of this.configurations()) { + config.reset() + } + // Require re-processing of file references + this.invalidateReferencedFiles() } - // Require re-processing of file references - this.invalidateReferencedFiles() - }) + ) .publishReplay() .refCount() as Observable } @@ -742,9 +751,15 @@ export class ProjectManager implements Disposable { public ensureOwnFiles(childOf = new Span()): Observable { return traceObservable('Ensure own files', childOf, span => { if (!this.ensuredOwnFiles) { - this.ensuredOwnFiles = this.updater.ensureStructure(span) + this.ensuredOwnFiles = this.updater + .ensureStructure(span) .concat(Observable.defer(() => observableFromIterable(this.inMemoryFs.uris()))) - .filter(uri => !uri.includes('/node_modules/') && isJSTSFile(uri) || isConfigFile(uri) || isPackageJsonFile(uri)) + .filter( + uri => + (!uri.includes('/node_modules/') && isJSTSFile(uri)) || + isConfigFile(uri) || + isPackageJsonFile(uri) + ) .mergeMap(uri => this.updater.ensure(uri)) .do(noop, err => { this.ensuredOwnFiles = undefined @@ -763,7 +778,8 @@ export class ProjectManager implements Disposable { public ensureAllFiles(childOf = new Span()): Observable { return traceObservable('Ensure all files', childOf, span => { if (!this.ensuredAllFiles) { - this.ensuredAllFiles = this.updater.ensureStructure(span) + this.ensuredAllFiles = this.updater + .ensureStructure(span) .concat(Observable.defer(() => observableFromIterable(this.inMemoryFs.uris()))) .filter(uri => isJSTSFile(uri) || isConfigFile(uri) || isPackageJsonFile(uri)) .mergeMap(uri => this.updater.ensure(uri)) @@ -794,25 +810,36 @@ export class ProjectManager implements Disposable { * @param childOf OpenTracing parent span for tracing * @return Observable of file URIs ensured */ - public ensureReferencedFiles(uri: string, maxDepth = 30, ignore = new Set(), childOf = new Span()): Observable { + public ensureReferencedFiles( + uri: string, + maxDepth = 30, + ignore = new Set(), + childOf = new Span() + ): Observable { return traceObservable('Ensure referenced files', childOf, span => { span.addTags({ uri, maxDepth }) ignore.add(uri) - return this.ensureModuleStructure(span) - .concat(Observable.defer(() => this.ensureConfigDependencies())) - // If max depth was reached, don't go any further - .concat(Observable.defer(() => maxDepth === 0 ? Observable.empty() : this.resolveReferencedFiles(uri))) - // Prevent cycles - .filter(referencedUri => !ignore.has(referencedUri)) - // Call method recursively with one less dep level - .mergeMap(referencedUri => - this.ensureReferencedFiles(referencedUri, maxDepth - 1, ignore) - // Continue even if an import wasn't found - .catch(err => { - this.logger.error(`Error resolving file references for ${uri}:`, err) - return [] - }) - ) + return ( + this.ensureModuleStructure(span) + .concat(Observable.defer(() => this.ensureConfigDependencies())) + // If max depth was reached, don't go any further + .concat( + Observable.defer( + () => (maxDepth === 0 ? Observable.empty() : this.resolveReferencedFiles(uri)) + ) + ) + // Prevent cycles + .filter(referencedUri => !ignore.has(referencedUri)) + // Call method recursively with one less dep level + .mergeMap(referencedUri => + this.ensureReferencedFiles(referencedUri, maxDepth - 1, ignore) + // Continue even if an import wasn't found + .catch(err => { + this.logger.error(`Error resolving file references for ${uri}:`, err) + return [] + }) + ) + ) }) } @@ -837,13 +864,13 @@ export class ProjectManager implements Disposable { return traceObservable('Ensure config dependencies', childOf, span => { if (!this.ensuredConfigDependencies) { this.ensuredConfigDependencies = observableFromIterable(this.inMemoryFs.uris()) - .filter(uri => this.isConfigDependency(toUnixPath(uri2path(uri)))) - .mergeMap(uri => this.updater.ensure(uri)) - .do(noop, err => { - this.ensuredConfigDependencies = undefined - }) - .publishReplay() - .refCount() as Observable + .filter(uri => this.isConfigDependency(toUnixPath(uri2path(uri)))) + .mergeMap(uri => this.updater.ensure(uri)) + .do(noop, err => { + this.ensuredConfigDependencies = undefined + }) + .publishReplay() + .refCount() as Observable } return this.ensuredConfigDependencies }) @@ -874,36 +901,66 @@ export class ProjectManager implements Disposable { if (observable) { return observable } - observable = this.updater.ensure(uri) - .concat(Observable.defer(() => { - const referencingFilePath = uri2path(uri) - const config = this.getConfiguration(referencingFilePath) - config.ensureBasicFiles(span) - const contents = this.inMemoryFs.getContent(uri) - const info = ts.preProcessFile(contents, true, true) - const compilerOpt = config.getHost().getCompilationSettings() - const pathResolver = referencingFilePath.includes('\\') ? path.win32 : path.posix - // Iterate imported files - return Observable.merge( - // References with `import` - Observable.from(info.importedFiles) - .map(importedFile => ts.resolveModuleName(importedFile.fileName, toUnixPath(referencingFilePath), compilerOpt, this.inMemoryFs)) - // false means we didn't find a file defining the module. It could still - // exist as an ambient module, which is why we fetch global*.d.ts files. - .filter(resolved => !!(resolved && resolved.resolvedModule)) - .map(resolved => resolved.resolvedModule!.resolvedFileName), - // References with `` - Observable.from(info.referencedFiles) - // Resolve triple slash references relative to current file instead of using - // module resolution host because it behaves differently in "nodejs" mode - .map(referencedFile => pathResolver.resolve(this.rootPath, pathResolver.dirname(referencingFilePath), toUnixPath(referencedFile.fileName))), - // References with `` - Observable.from(info.typeReferenceDirectives) - .map(typeReferenceDirective => ts.resolveTypeReferenceDirective(typeReferenceDirective.fileName, referencingFilePath, compilerOpt, this.inMemoryFs)) - .filter(resolved => !!(resolved && resolved.resolvedTypeReferenceDirective && resolved.resolvedTypeReferenceDirective.resolvedFileName)) - .map(resolved => resolved.resolvedTypeReferenceDirective!.resolvedFileName!) - ) - })) + observable = this.updater + .ensure(uri) + .concat( + Observable.defer(() => { + const referencingFilePath = uri2path(uri) + const config = this.getConfiguration(referencingFilePath) + config.ensureBasicFiles(span) + const contents = this.inMemoryFs.getContent(uri) + const info = ts.preProcessFile(contents, true, true) + const compilerOpt = config.getHost().getCompilationSettings() + const pathResolver = referencingFilePath.includes('\\') ? path.win32 : path.posix + // Iterate imported files + return Observable.merge( + // References with `import` + Observable.from(info.importedFiles) + .map(importedFile => + ts.resolveModuleName( + importedFile.fileName, + toUnixPath(referencingFilePath), + compilerOpt, + this.inMemoryFs + ) + ) + // false means we didn't find a file defining the module. It could still + // exist as an ambient module, which is why we fetch global*.d.ts files. + .filter(resolved => !!(resolved && resolved.resolvedModule)) + .map(resolved => resolved.resolvedModule!.resolvedFileName), + // References with `` + Observable.from(info.referencedFiles) + // Resolve triple slash references relative to current file instead of using + // module resolution host because it behaves differently in "nodejs" mode + .map(referencedFile => + pathResolver.resolve( + this.rootPath, + pathResolver.dirname(referencingFilePath), + toUnixPath(referencedFile.fileName) + ) + ), + // References with `` + Observable.from(info.typeReferenceDirectives) + .map(typeReferenceDirective => + ts.resolveTypeReferenceDirective( + typeReferenceDirective.fileName, + referencingFilePath, + compilerOpt, + this.inMemoryFs + ) + ) + .filter( + resolved => + !!( + resolved && + resolved.resolvedTypeReferenceDirective && + resolved.resolvedTypeReferenceDirective.resolvedFileName + ) + ) + .map(resolved => resolved.resolvedTypeReferenceDirective!.resolvedFileName!) + ) + }) + ) // Use same scheme, slashes, host for referenced URI as input file .map(path2uri) // Don't cache errors @@ -921,7 +978,10 @@ export class ProjectManager implements Disposable { * @param filePath source file path, absolute * @return project configuration for a given source file. Climbs directory tree up to workspace root if needed */ - public getConfiguration(filePath: string, configType: ConfigType = this.getConfigurationType(filePath)): ProjectConfiguration { + public getConfiguration( + filePath: string, + configType: ConfigType = this.getConfigurationType(filePath) + ): ProjectConfiguration { const config = this.getConfigurationIfExists(filePath, configType) if (!config) { throw new Error(`TypeScript config file for ${filePath} not found`) @@ -933,7 +993,10 @@ export class ProjectManager implements Disposable { * @param filePath source file path, absolute * @return closest configuration for a given file path or undefined if there is no such configuration */ - public getConfigurationIfExists(filePath: string, configType = this.getConfigurationType(filePath)): ProjectConfiguration | undefined { + public getConfigurationIfExists( + filePath: string, + configType = this.getConfigurationType(filePath) + ): ProjectConfiguration | undefined { let dir = filePath let config: ProjectConfiguration | undefined const configs = this.configs[configType] @@ -970,7 +1033,8 @@ export class ProjectManager implements Disposable { */ public getChildConfigurations(uri: string): IterableIterator { const pathPrefix = uri2path(uri) - return iterate(this.configs.ts).concat(this.configs.js) + return iterate(this.configs.ts) + .concat(this.configs.js) .filter(([folderPath, config]) => folderPath.startsWith(pathPrefix)) .map(([folderPath, config]) => config) } diff --git a/src/request-type.ts b/src/request-type.ts index 541cbdc3a..7323bbf64 100644 --- a/src/request-type.ts +++ b/src/request-type.ts @@ -15,7 +15,6 @@ export interface PluginSettings { } export interface ClientCapabilities extends vscode.ClientCapabilities { - /** * The client provides support for workspace/xfiles. */ @@ -54,7 +53,6 @@ export interface InitializeResult extends vscode.InitializeResult { } export interface TextDocumentContentParams { - /** * The text document to receive the content for. */ @@ -62,7 +60,6 @@ export interface TextDocumentContentParams { } export interface WorkspaceFilesParams { - /** * The URI of a directory to search. * Can be relative to the rootPath. @@ -80,7 +77,6 @@ export interface WorkspaceFilesParams { * metadata about the symbol. */ export interface SymbolDescriptor { - /** * The kind of the symbol as a ts.ScriptElementKind */ @@ -138,7 +134,6 @@ export interface WorkspaceSymbolParams { * spec). */ export interface WorkspaceReferenceParams { - /** * Metadata about the symbol that is being searched for. */ @@ -153,7 +148,6 @@ export interface WorkspaceReferenceParams { } export interface SymbolLocationInformation { - /** * The location where the symbol is defined, if any */ @@ -206,7 +200,6 @@ export interface DependencyReference { * identified by the provided key. */ export interface CacheGetParams { - /** * The key that identifies the cache item */ @@ -219,7 +212,6 @@ export interface CacheGetParams { * the server is not supposed to act differently if the cache set failed. */ export interface CacheSetParams { - /** * The key that identifies the cache item */ diff --git a/src/server.ts b/src/server.ts index 535c8287a..a8dc15781 100644 --- a/src/server.ts +++ b/src/server.ts @@ -9,7 +9,6 @@ import { TypeScriptService } from './typescript-service' /** Options to `serve()` */ export interface ServeOptions extends MessageLogOptions { - /** Amount of workers to spawn */ clusterSize: number @@ -36,7 +35,10 @@ export function createClusterLogger(logger = new StdioLogger()): Logger { * @param options * @param createLangHandler Factory function that is called for each new connection */ -export function serve(options: ServeOptions, createLangHandler = (remoteClient: RemoteLanguageClient) => new TypeScriptService(remoteClient)): void { +export function serve( + options: ServeOptions, + createLangHandler = (remoteClient: RemoteLanguageClient) => new TypeScriptService(remoteClient) +): void { const logger = options.logger || createClusterLogger() if (options.clusterSize > 1 && cluster.isMaster) { logger.log(`Spawning ${options.clusterSize} workers`) @@ -44,7 +46,10 @@ export function serve(options: ServeOptions, createLangHandler = (remoteClient: logger.log(`Worker ${worker.id} (PID ${worker.process.pid}) online`) }) cluster.on('exit', (worker, code, signal) => { - logger.error(`Worker ${worker.id} (PID ${worker.process.pid}) exited from signal ${signal} with code ${code}, restarting`) + logger.error( + `Worker ${worker.id} (PID ${worker.process + .pid}) exited from signal ${signal} with code ${code}, restarting` + ) cluster.fork() }) for (let i = 0; i < options.clusterSize; ++i) { diff --git a/src/symbols.ts b/src/symbols.ts index 7545d0559..8119b90dd 100644 --- a/src/symbols.ts +++ b/src/symbols.ts @@ -1,4 +1,3 @@ - import * as ts from 'typescript' import { SymbolInformation, SymbolKind } from 'vscode-languageserver-types' import { isTypeScriptLibrary } from './memfs' @@ -47,7 +46,11 @@ export function locationUri(filePath: string): string { * * @param rootPath The workspace rootPath to remove from symbol names and containerNames */ -export function navigateToItemToSymbolInformation(item: ts.NavigateToItem, program: ts.Program, rootPath: string): SymbolInformation { +export function navigateToItemToSymbolInformation( + item: ts.NavigateToItem, + program: ts.Program, + rootPath: string +): SymbolInformation { const sourceFile = program.getSourceFile(item.fileName) if (!sourceFile) { throw new Error(`Source file ${item.fileName} does not exist`) @@ -74,27 +77,48 @@ export function navigateToItemToSymbolInformation(item: ts.NavigateToItem, progr */ export function stringtoSymbolKind(kind: string): SymbolKind { switch (kind) { - case 'module': return SymbolKind.Module - case 'class': return SymbolKind.Class - case 'local class': return SymbolKind.Class - case 'interface': return SymbolKind.Interface - case 'enum': return SymbolKind.Enum - case 'enum member': return SymbolKind.Constant - case 'var': return SymbolKind.Variable - case 'local var': return SymbolKind.Variable - case 'function': return SymbolKind.Function - case 'local function': return SymbolKind.Function - case 'method': return SymbolKind.Method - case 'getter': return SymbolKind.Method - case 'setter': return SymbolKind.Method - case 'property': return SymbolKind.Property - case 'constructor': return SymbolKind.Constructor - case 'parameter': return SymbolKind.Variable - case 'type parameter': return SymbolKind.Variable - case 'alias': return SymbolKind.Variable - case 'let': return SymbolKind.Variable - case 'const': return SymbolKind.Constant - case 'JSX attribute': return SymbolKind.Property + case 'module': + return SymbolKind.Module + case 'class': + return SymbolKind.Class + case 'local class': + return SymbolKind.Class + case 'interface': + return SymbolKind.Interface + case 'enum': + return SymbolKind.Enum + case 'enum member': + return SymbolKind.Constant + case 'var': + return SymbolKind.Variable + case 'local var': + return SymbolKind.Variable + case 'function': + return SymbolKind.Function + case 'local function': + return SymbolKind.Function + case 'method': + return SymbolKind.Method + case 'getter': + return SymbolKind.Method + case 'setter': + return SymbolKind.Method + case 'property': + return SymbolKind.Property + case 'constructor': + return SymbolKind.Constructor + case 'parameter': + return SymbolKind.Variable + case 'type parameter': + return SymbolKind.Variable + case 'alias': + return SymbolKind.Variable + case 'let': + return SymbolKind.Variable + case 'const': + return SymbolKind.Constant + case 'JSX attribute': + return SymbolKind.Property // case 'script' // case 'keyword' // case 'type' @@ -106,14 +130,20 @@ export function stringtoSymbolKind(kind: string): SymbolKind { // case 'directory' // case 'external module name' // case 'external module name' - default: return SymbolKind.Variable + default: + return SymbolKind.Variable } } /** * Returns an LSP SymbolInformation for a TypeScript NavigationTree node */ -export function navigationTreeToSymbolInformation(tree: ts.NavigationTree, parent: ts.NavigationTree | undefined, sourceFile: ts.SourceFile, rootPath: string): SymbolInformation { +export function navigationTreeToSymbolInformation( + tree: ts.NavigationTree, + parent: ts.NavigationTree | undefined, + sourceFile: ts.SourceFile, + rootPath: string +): SymbolInformation { const span = tree.spans[0] if (!span) { throw new Error('NavigationTree has no TextSpan') @@ -138,7 +168,12 @@ export function navigationTreeToSymbolInformation(tree: ts.NavigationTree, paren /** * Returns a SymbolDescriptor for a TypeScript NavigationTree node */ -export function navigationTreeToSymbolDescriptor(tree: ts.NavigationTree, parent: ts.NavigationTree | undefined, filePath: string, rootPath: string): SymbolDescriptor { +export function navigationTreeToSymbolDescriptor( + tree: ts.NavigationTree, + parent: ts.NavigationTree | undefined, + filePath: string, + rootPath: string +): SymbolDescriptor { const symbolDescriptor: SymbolDescriptor = { kind: tree.kind, name: tree.text ? tree.text.replace(rootPath, '') : '', @@ -169,7 +204,10 @@ export function navigationTreeToSymbolDescriptor(tree: ts.NavigationTree, parent /** * Walks a NaviationTree and emits items with a node and its parent node (if exists) */ -export function *walkNavigationTree(tree: ts.NavigationTree, parent?: ts.NavigationTree): IterableIterator<{ tree: ts.NavigationTree, parent?: ts.NavigationTree }> { +export function* walkNavigationTree( + tree: ts.NavigationTree, + parent?: ts.NavigationTree +): IterableIterator<{ tree: ts.NavigationTree; parent?: ts.NavigationTree }> { yield { tree, parent } for (const childItem of tree.childItems || []) { yield* walkNavigationTree(childItem, tree) diff --git a/src/test/connection.test.ts b/src/test/connection.test.ts index 69e50044f..45dbebe7b 100644 --- a/src/test/connection.test.ts +++ b/src/test/connection.test.ts @@ -1,4 +1,3 @@ - import * as assert from 'assert' import { EventEmitter } from 'events' import { Operation } from 'fast-json-patch' @@ -19,11 +18,18 @@ describe('connection', () => { const writer = { write: sinon.spy(), } - registerLanguageHandler(emitter as MessageEmitter, writer as any as MessageWriter, handler as TypeScriptService) + registerLanguageHandler( + emitter as MessageEmitter, + (writer as any) as MessageWriter, + handler as TypeScriptService + ) const params = [1, 1] emitter.emit('message', { jsonrpc: '2.0', id: 1, method: 'whatever', params }) sinon.assert.calledOnce(writer.write) - sinon.assert.calledWithExactly(writer.write, sinon.match({ jsonrpc: '2.0', id: 1, error: { code: ErrorCodes.MethodNotFound } })) + sinon.assert.calledWithExactly( + writer.write, + sinon.match({ jsonrpc: '2.0', id: 1, error: { code: ErrorCodes.MethodNotFound } }) + ) }) it('should return MethodNotFound error when the method is prefixed with an underscore', async () => { const handler = { _privateMethod: sinon.spy() } @@ -36,11 +42,16 @@ describe('connection', () => { emitter.emit('message', { jsonrpc: '2.0', id: 1, method: 'textDocument/hover', params }) sinon.assert.notCalled(handler._privateMethod) sinon.assert.calledOnce(writer.write) - sinon.assert.calledWithExactly(writer.write, sinon.match({ jsonrpc: '2.0', id: 1, error: { code: ErrorCodes.MethodNotFound } })) + sinon.assert.calledWithExactly( + writer.write, + sinon.match({ jsonrpc: '2.0', id: 1, error: { code: ErrorCodes.MethodNotFound } }) + ) }) it('should call a handler on request and send the result of the returned Promise', async () => { - const handler: { [K in keyof TypeScriptService]: TypeScriptService[K] & sinon.SinonStub } = sinon.createStubInstance(TypeScriptService) - handler.initialize.returns(Promise.resolve({ op: 'add', path: '', value: { capabilities: {} }})) + const handler: { + [K in keyof TypeScriptService]: TypeScriptService[K] & sinon.SinonStub + } = sinon.createStubInstance(TypeScriptService) + handler.initialize.returns(Promise.resolve({ op: 'add', path: '', value: { capabilities: {} } })) handler.textDocumentHover.returns(Promise.resolve(2)) const emitter = new EventEmitter() const writer = { @@ -50,7 +61,10 @@ describe('connection', () => { emitter.emit('message', { jsonrpc: '2.0', id: 1, method: 'initialize', params: { capabilities: {} } }) await new Promise(resolve => setTimeout(resolve, 0)) sinon.assert.calledOnce(handler.initialize) - sinon.assert.calledWithExactly(writer.write, sinon.match({ jsonrpc: '2.0', id: 1, result: { capabilities: {} } })) + sinon.assert.calledWithExactly( + writer.write, + sinon.match({ jsonrpc: '2.0', id: 1, result: { capabilities: {} } }) + ) }) it('should ignore exit notifications', async () => { const handler = { @@ -93,10 +107,11 @@ describe('connection', () => { }) it('should call a handler on request and send the result of the returned Observable', async () => { const handler: TypeScriptService = Object.create(TypeScriptService.prototype) - const hoverStub = sinon.stub(handler, 'textDocumentHover').returns(Observable.of( - { op: 'add', path: '', value: [] }, - { op: 'add', path: '/-', value: 123 } - )) + const hoverStub = sinon + .stub(handler, 'textDocumentHover') + .returns( + Observable.of({ op: 'add', path: '', value: [] }, { op: 'add', path: '/-', value: 123 }) + ) const emitter = new EventEmitter() const writer = { write: sinon.spy(), @@ -110,10 +125,14 @@ describe('connection', () => { }) it('should call a handler on request and send the thrown error of the returned Observable', async () => { const handler: TypeScriptService = Object.create(TypeScriptService.prototype) - const hoverStub = sinon.stub(handler, 'textDocumentHover').returns(Observable.throw(Object.assign(new Error('Something happened'), { - code: ErrorCodes.serverErrorStart, - whatever: 123, - }))) + const hoverStub = sinon.stub(handler, 'textDocumentHover').returns( + Observable.throw( + Object.assign(new Error('Something happened'), { + code: ErrorCodes.serverErrorStart, + whatever: 123, + }) + ) + ) const emitter = new EventEmitter() const writer = { write: sinon.spy(), @@ -124,19 +143,24 @@ describe('connection', () => { sinon.assert.calledOnce(hoverStub) sinon.assert.calledWithExactly(hoverStub, params, sinon.match.instanceOf(Span)) sinon.assert.calledOnce(writer.write) - sinon.assert.calledWithExactly(writer.write, sinon.match({ - jsonrpc: '2.0', - id: 1, - error: { - message: 'Something happened', - code: ErrorCodes.serverErrorStart, - data: { whatever: 123 }, - }, - })) + sinon.assert.calledWithExactly( + writer.write, + sinon.match({ + jsonrpc: '2.0', + id: 1, + error: { + message: 'Something happened', + code: ErrorCodes.serverErrorStart, + data: { whatever: 123 }, + }, + }) + ) }) it('should call a handler on request and send the returned synchronous value', async () => { const handler: TypeScriptService = Object.create(TypeScriptService.prototype) - const hoverStub = sinon.stub(handler, 'textDocumentHover').returns(Observable.of({ op: 'add', path: '', value: 2 })) + const hoverStub = sinon + .stub(handler, 'textDocumentHover') + .returns(Observable.of({ op: 'add', path: '', value: 2 })) const emitter = new EventEmitter() const writer = { write: sinon.spy(), @@ -149,7 +173,9 @@ describe('connection', () => { }) it('should call a handler on request and send the result of the returned Observable', async () => { const handler: TypeScriptService = Object.create(TypeScriptService.prototype) - const hoverStub = sinon.stub(handler, 'textDocumentHover').returns(Observable.of({ op: 'add', path: '', value: 2 })) + const hoverStub = sinon + .stub(handler, 'textDocumentHover') + .returns(Observable.of({ op: 'add', path: '', value: 2 })) const emitter = new EventEmitter() const writer = { write: sinon.spy(), @@ -164,7 +190,9 @@ describe('connection', () => { it('should unsubscribe from the returned Observable when $/cancelRequest was sent and return a RequestCancelled error', async () => { const handler: TypeScriptService = Object.create(TypeScriptService.prototype) const unsubscribeHandler = sinon.spy() - const hoverStub = sinon.stub(handler, 'textDocumentHover').returns(new Observable(subscriber => unsubscribeHandler)) + const hoverStub = sinon + .stub(handler, 'textDocumentHover') + .returns(new Observable(subscriber => unsubscribeHandler)) const emitter = new EventEmitter() const writer = { write: sinon.spy(), @@ -177,12 +205,17 @@ describe('connection', () => { emitter.emit('message', { jsonrpc: '2.0', method: '$/cancelRequest', params: { id: 1 } }) sinon.assert.calledOnce(unsubscribeHandler) sinon.assert.calledOnce(writer.write) - sinon.assert.calledWithExactly(writer.write, sinon.match({ jsonrpc: '2.0', id: 1, error: { code: ErrorCodes.RequestCancelled } })) + sinon.assert.calledWithExactly( + writer.write, + sinon.match({ jsonrpc: '2.0', id: 1, error: { code: ErrorCodes.RequestCancelled } }) + ) }) it('should unsubscribe from the returned Observable when the connection was closed', async () => { const handler: TypeScriptService = Object.create(TypeScriptService.prototype) const unsubscribeHandler = sinon.spy() - const hoverStub = sinon.stub(handler, 'textDocumentHover').returns(new Observable(subscriber => unsubscribeHandler)) + const hoverStub = sinon + .stub(handler, 'textDocumentHover') + .returns(new Observable(subscriber => unsubscribeHandler)) const emitter = new EventEmitter() const writer = { write: sinon.spy(), @@ -197,7 +230,9 @@ describe('connection', () => { it('should unsubscribe from the returned Observable on exit notification', async () => { const handler: TypeScriptService = Object.create(TypeScriptService.prototype) const unsubscribeHandler = sinon.spy() - const hoverStub = sinon.stub(handler, 'textDocumentHover').returns(new Observable(subscriber => unsubscribeHandler)) + const hoverStub = sinon + .stub(handler, 'textDocumentHover') + .returns(new Observable(subscriber => unsubscribeHandler)) const emitter = new EventEmitter() const writer = { write: sinon.spy(), @@ -212,7 +247,9 @@ describe('connection', () => { for (const event of ['close', 'error']) { it(`should call shutdown on ${event} if the service was initialized`, async () => { const handler = { - initialize: sinon.stub().returns(Observable.of({ op: 'add', path: '', value: { capabilities: {} }})), + initialize: sinon + .stub() + .returns(Observable.of({ op: 'add', path: '', value: { capabilities: {} } })), shutdown: sinon.stub().returns(Observable.of({ op: 'add', path: '', value: null })), } const emitter = new EventEmitter() @@ -228,7 +265,9 @@ describe('connection', () => { }) it(`should not call shutdown on ${event} if the service was not initialized`, async () => { const handler = { - initialize: sinon.stub().returns(Observable.of({ op: 'add', path: '', value: { capabilities: {} }})), + initialize: sinon + .stub() + .returns(Observable.of({ op: 'add', path: '', value: { capabilities: {} } })), shutdown: sinon.stub().returns(Observable.of({ op: 'add', path: '', value: null })), } const emitter = new EventEmitter() @@ -241,7 +280,9 @@ describe('connection', () => { }) it(`should not call shutdown again on ${event} if shutdown was already called`, async () => { const handler = { - initialize: sinon.stub().returns(Observable.of({ op: 'add', path: '', value: { capabilities: {} }})), + initialize: sinon + .stub() + .returns(Observable.of({ op: 'add', path: '', value: { capabilities: {} } })), shutdown: sinon.stub().returns(Observable.of({ op: 'add', path: '', value: null })), } const emitter = new EventEmitter() @@ -258,8 +299,12 @@ describe('connection', () => { } describe('Client with streaming support', () => { it('should call a handler on request and send partial results of the returned Observable', async () => { - const handler: { [K in keyof TypeScriptService]: TypeScriptService[K] & sinon.SinonStub } = sinon.createStubInstance(TypeScriptService) - handler.initialize.returns(Observable.of({ op: 'add', path: '', value: { capabilities: { streaming: true }}})) + const handler: { + [K in keyof TypeScriptService]: TypeScriptService[K] & sinon.SinonStub + } = sinon.createStubInstance(TypeScriptService) + handler.initialize.returns( + Observable.of({ op: 'add', path: '', value: { capabilities: { streaming: true } } }) + ) const hoverSubject = new Subject() handler.textDocumentHover.returns(hoverSubject) @@ -272,20 +317,37 @@ describe('connection', () => { registerLanguageHandler(emitter as MessageEmitter, writer as any, handler as any) // Send initialize - emitter.emit('message', { jsonrpc: '2.0', id: 1, method: 'initialize', params: { capabilities: { streaming: true }}}) - assert.deepEqual(writer.write.args[0], [{ - jsonrpc: '2.0', - method: '$/partialResult', - params: { - id: 1, - patch: [{ op: 'add', path: '', value: { capabilities: { streaming: true }}}], - }, - }], 'Expected to send partial result for initialize') - assert.deepEqual(writer.write.args[1], [{ + emitter.emit('message', { jsonrpc: '2.0', id: 1, - result: { capabilities: { streaming: true } }, - }], 'Expected to send final result for initialize') + method: 'initialize', + params: { capabilities: { streaming: true } }, + }) + assert.deepEqual( + writer.write.args[0], + [ + { + jsonrpc: '2.0', + method: '$/partialResult', + params: { + id: 1, + patch: [{ op: 'add', path: '', value: { capabilities: { streaming: true } } }], + }, + }, + ], + 'Expected to send partial result for initialize' + ) + assert.deepEqual( + writer.write.args[1], + [ + { + jsonrpc: '2.0', + id: 1, + result: { capabilities: { streaming: true } }, + }, + ], + 'Expected to send final result for initialize' + ) // Send hover emitter.emit('message', { jsonrpc: '2.0', id: 2, method: 'textDocument/hover', params: [1, 2] }) @@ -293,27 +355,45 @@ describe('connection', () => { // Simulate initializing JSON Patch Operation hoverSubject.next({ op: 'add', path: '', value: [] }) - assert.deepEqual(writer.write.args[2], [{ - jsonrpc: '2.0', - method: '$/partialResult', - params: { id: 2, patch: [{ op: 'add', path: '', value: [] }] }, - }], 'Expected to send partial result that initializes array') + assert.deepEqual( + writer.write.args[2], + [ + { + jsonrpc: '2.0', + method: '$/partialResult', + params: { id: 2, patch: [{ op: 'add', path: '', value: [] }] }, + }, + ], + 'Expected to send partial result that initializes array' + ) // Simulate streamed value hoverSubject.next({ op: 'add', path: '/-', value: 123 }) - assert.deepEqual(writer.write.args[3], [{ - jsonrpc: '2.0', - method: '$/partialResult', - params: { id: 2, patch: [{ op: 'add', path: '/-', value: 123 }] }, - }], 'Expected to send partial result that adds 123 to array') + assert.deepEqual( + writer.write.args[3], + [ + { + jsonrpc: '2.0', + method: '$/partialResult', + params: { id: 2, patch: [{ op: 'add', path: '/-', value: 123 }] }, + }, + ], + 'Expected to send partial result that adds 123 to array' + ) // Complete Subject to trigger final response hoverSubject.complete() - assert.deepEqual(writer.write.args[4], [{ - jsonrpc: '2.0', - id: 2, - result: [123], - }], 'Expected to send final result [123]') + assert.deepEqual( + writer.write.args[4], + [ + { + jsonrpc: '2.0', + id: 2, + result: [123], + }, + ], + 'Expected to send final result [123]' + ) }) }) }) diff --git a/src/test/fs-helpers.ts b/src/test/fs-helpers.ts index 4323bf656..910022cf7 100644 --- a/src/test/fs-helpers.ts +++ b/src/test/fs-helpers.ts @@ -6,12 +6,10 @@ import { observableFromIterable } from '../util' * Map-based file system that holds map (URI -> content) */ export class MapFileSystem implements FileSystem { - - constructor(private files: Map) { } + constructor(private files: Map) {} public getWorkspaceFiles(base?: string): Observable { - return observableFromIterable(this.files.keys()) - .filter(path => !base || path.startsWith(base)) + return observableFromIterable(this.files.keys()).filter(path => !base || path.startsWith(base)) } public getTextDocumentContent(uri: string): Observable { diff --git a/src/test/fs.test.ts b/src/test/fs.test.ts index 8144ae7d0..8ca580e27 100644 --- a/src/test/fs.test.ts +++ b/src/test/fs.test.ts @@ -17,7 +17,7 @@ describe('fs.ts', () => { before(async () => { temporaryDir = await new Promise((resolve, reject) => { - temp.mkdir('local-fs', (err: Error, dirPath: string) => err ? reject(err) : resolve(dirPath)) + temp.mkdir('local-fs', (err: Error, dirPath: string) => (err ? reject(err) : resolve(dirPath))) }) // global packages contains a package @@ -47,13 +47,16 @@ describe('fs.ts', () => { }) after(async () => { await new Promise((resolve, reject) => { - rimraf(temporaryDir, err => err ? reject(err) : resolve()) + rimraf(temporaryDir, err => (err ? reject(err) : resolve())) }) }) describe('getWorkspaceFiles()', () => { it('should return all files in the workspace', async () => { - const files = await fileSystem.getWorkspaceFiles().toArray().toPromise() + const files = await fileSystem + .getWorkspaceFiles() + .toArray() + .toPromise() assert.sameMembers(files, [ rootUri + 'tweedledee', rootUri + 'tweedledum', @@ -63,10 +66,11 @@ describe('fs.ts', () => { ]) }) it('should return all files under specific root', async () => { - const files = await fileSystem.getWorkspaceFiles(rootUri + 'foo').toArray().toPromise() - assert.sameMembers(files, [ - rootUri + 'foo/bar.ts', - ]) + const files = await fileSystem + .getWorkspaceFiles(rootUri + 'foo') + .toArray() + .toPromise() + assert.sameMembers(files, [rootUri + 'foo/bar.ts']) }) }) describe('getTextDocumentContent()', () => { diff --git a/src/test/plugins.test.ts b/src/test/plugins.test.ts index 4eb21552c..98bd522d8 100644 --- a/src/test/plugins.test.ts +++ b/src/test/plugins.test.ts @@ -15,14 +15,16 @@ describe('plugins', () => { const compilerOptions: ts.CompilerOptions = {} const applyProxy: (pluginModuleFactory: PluginModuleFactory) => PluginModule = sinon.spy() loader.loadPlugins(compilerOptions, applyProxy) - }) it('should load a global plugin if specified', () => { const memfs = new InMemoryFileSystem('/') const peerPackagesPath = path.resolve(__filename, '../../../../') const peerPackagesUri = path2uri(peerPackagesPath) - memfs.add(peerPackagesUri + '/node_modules/some-plugin/package.json', '{ "name": "some-plugin", "version": "0.1.1", "main": "plugin.js"}') + memfs.add( + peerPackagesUri + '/node_modules/some-plugin/package.json', + '{ "name": "some-plugin", "version": "0.1.1", "main": "plugin.js"}' + ) memfs.add(peerPackagesUri + '/node_modules/some-plugin/plugin.js', '') const pluginSettings: PluginSettings = { globalPlugins: ['some-plugin'], @@ -36,14 +38,21 @@ describe('plugins', () => { const applyProxy = sinon.spy() loader.loadPlugins(compilerOptions, applyProxy) sinon.assert.calledOnce(applyProxy) - sinon.assert.calledWithExactly(applyProxy, pluginFactoryFunc, sinon.match({ name: 'some-plugin', global: true})) + sinon.assert.calledWithExactly( + applyProxy, + pluginFactoryFunc, + sinon.match({ name: 'some-plugin', global: true }) + ) }) it('should load a local plugin if specified', () => { const rootDir = (process.platform === 'win32' ? 'c:\\' : '/') + 'some-project' const rootUri = path2uri(rootDir) + '/' const memfs = new InMemoryFileSystem('/some-project') - memfs.add(rootUri + 'node_modules/some-plugin/package.json', '{ "name": "some-plugin", "version": "0.1.1", "main": "plugin.js"}') + memfs.add( + rootUri + 'node_modules/some-plugin/package.json', + '{ "name": "some-plugin", "version": "0.1.1", "main": "plugin.js"}' + ) memfs.add(rootUri + 'node_modules/some-plugin/plugin.js', '') const pluginSettings: PluginSettings = { globalPlugins: [], @@ -64,6 +73,5 @@ describe('plugins', () => { sinon.assert.calledOnce(applyProxy) sinon.assert.calledWithExactly(applyProxy, pluginFactoryFunc, sinon.match(pluginOption)) }) - }) }) diff --git a/src/test/project-manager.test.ts b/src/test/project-manager.test.ts index d8d47ff2f..15e8e0c8d 100644 --- a/src/test/project-manager.test.ts +++ b/src/test/project-manager.test.ts @@ -11,7 +11,6 @@ const assert = chai.assert describe('ProjectManager', () => { for (const rootUri of ['file:///', 'file:///c:/foo/bar/', 'file:///foo/bar/']) { describe(`with rootUri ${rootUri}`, () => { - let projectManager: ProjectManager let memfs: InMemoryFileSystem @@ -19,9 +18,7 @@ describe('ProjectManager', () => { const rootPath = uri2path(rootUri) memfs = new InMemoryFileSystem(rootPath) const configFileUri = rootUri + 'foo/tsconfig.json' - const localfs = new MapFileSystem(new Map([ - [configFileUri, '{}'], - ])) + const localfs = new MapFileSystem(new Map([[configFileUri, '{}']])) const updater = new FileSystemUpdater(localfs, memfs) projectManager = new ProjectManager(rootPath, memfs, updater, true) memfs.add(configFileUri, '{}') @@ -35,14 +32,18 @@ describe('ProjectManager', () => { beforeEach(async () => { const rootPath = uri2path(rootUri) memfs = new InMemoryFileSystem(rootPath) - const localfs = new MapFileSystem(new Map([ - [rootUri + 'project/package.json', '{"name": "package-name-1"}'], - [rootUri + 'project/tsconfig.json', '{ "compilerOptions": { "typeRoots": ["../types"]} }'], - [rootUri + 'project/node_modules/%40types/mocha/index.d.ts', 'declare var describe { (description: string, spec: () => void): void; }'], - [rootUri + 'project/file.ts', 'describe("test", () => console.log(GLOBALCONSTANT));'], - [rootUri + 'types/types.d.ts', 'declare var GLOBALCONSTANT=1;'], - - ])) + const localfs = new MapFileSystem( + new Map([ + [rootUri + 'project/package.json', '{"name": "package-name-1"}'], + [rootUri + 'project/tsconfig.json', '{ "compilerOptions": { "typeRoots": ["../types"]} }'], + [ + rootUri + 'project/node_modules/%40types/mocha/index.d.ts', + 'declare var describe { (description: string, spec: () => void): void; }', + ], + [rootUri + 'project/file.ts', 'describe("test", () => console.log(GLOBALCONSTANT));'], + [rootUri + 'types/types.d.ts', 'declare var GLOBALCONSTANT=1;'], + ]) + ) const updater = new FileSystemUpdater(localfs, memfs) projectManager = new ProjectManager(rootPath, memfs, updater, true) }) @@ -76,12 +77,14 @@ describe('ProjectManager', () => { beforeEach(async () => { const rootPath = uri2path(rootUri) memfs = new InMemoryFileSystem(rootPath) - const localfs = new MapFileSystem(new Map([ - [rootUri + 'package.json', '{"name": "package-name-1"}'], - [rootUri + 'subdirectory-with-tsconfig/package.json', '{"name": "package-name-2"}'], - [rootUri + 'subdirectory-with-tsconfig/src/tsconfig.json', '{}'], - [rootUri + 'subdirectory-with-tsconfig/src/dummy.ts', ''], - ])) + const localfs = new MapFileSystem( + new Map([ + [rootUri + 'package.json', '{"name": "package-name-1"}'], + [rootUri + 'subdirectory-with-tsconfig/package.json', '{"name": "package-name-2"}'], + [rootUri + 'subdirectory-with-tsconfig/src/tsconfig.json', '{}'], + [rootUri + 'subdirectory-with-tsconfig/src/dummy.ts', ''], + ]) + ) const updater = new FileSystemUpdater(localfs, memfs) projectManager = new ProjectManager(rootPath, memfs, updater, true) await projectManager.ensureAllFiles().toPromise() @@ -92,13 +95,18 @@ describe('ProjectManager', () => { beforeEach(() => { const rootPath = uri2path(rootUri) memfs = new InMemoryFileSystem(rootPath) - const localfs = new MapFileSystem(new Map([ - [rootUri + 'package.json', '{"name": "package-name-1"}'], - [rootUri + 'node_modules/somelib/index.js', '/// \n/// '], - [rootUri + 'node_modules/somelib/pathref.d.ts', ''], - [rootUri + 'node_modules/%40types/node/index.d.ts', ''], - [rootUri + 'src/dummy.ts', 'import * as somelib from "somelib";'], - ])) + const localfs = new MapFileSystem( + new Map([ + [rootUri + 'package.json', '{"name": "package-name-1"}'], + [ + rootUri + 'node_modules/somelib/index.js', + '/// \n/// ', + ], + [rootUri + 'node_modules/somelib/pathref.d.ts', ''], + [rootUri + 'node_modules/%40types/node/index.d.ts', ''], + [rootUri + 'src/dummy.ts', 'import * as somelib from "somelib";'], + ]) + ) const updater = new FileSystemUpdater(localfs, memfs) projectManager = new ProjectManager(rootPath, memfs, updater, true) }) @@ -113,10 +121,9 @@ describe('ProjectManager', () => { beforeEach(async () => { const rootPath = uri2path(rootUri) memfs = new InMemoryFileSystem(rootPath) - const localfs = new MapFileSystem(new Map([ - [rootUri + 'tsconfig.json', '{}'], - [rootUri + 'src/jsconfig.json', '{}'], - ])) + const localfs = new MapFileSystem( + new Map([[rootUri + 'tsconfig.json', '{}'], [rootUri + 'src/jsconfig.json', '{}']]) + ) const updater = new FileSystemUpdater(localfs, memfs) projectManager = new ProjectManager(rootPath, memfs, updater, true) await projectManager.ensureAllFiles().toPromise() @@ -133,10 +140,9 @@ describe('ProjectManager', () => { beforeEach(async () => { const rootPath = uri2path(rootUri) memfs = new InMemoryFileSystem(rootPath) - const localfs = new MapFileSystem(new Map([ - [rootUri + 'tsconfig.json', '{}'], - [rootUri + 'src/jsconfig.json', '{}'], - ])) + const localfs = new MapFileSystem( + new Map([[rootUri + 'tsconfig.json', '{}'], [rootUri + 'src/jsconfig.json', '{}']]) + ) const updater = new FileSystemUpdater(localfs, memfs) projectManager = new ProjectManager(rootPath, memfs, updater, true) await projectManager.ensureAllFiles().toPromise() @@ -152,17 +158,21 @@ describe('ProjectManager', () => { beforeEach(async () => { const rootPath = uri2path(rootUri) memfs = new InMemoryFileSystem(rootPath) - const localfs = new MapFileSystem(new Map([ - [rootUri + 'tsconfig.json', '{}'], - [rootUri + 'foo/bar/tsconfig.json', '{}'], - [rootUri + 'foo/baz/tsconfig.json', '{}'], - ])) + const localfs = new MapFileSystem( + new Map([ + [rootUri + 'tsconfig.json', '{}'], + [rootUri + 'foo/bar/tsconfig.json', '{}'], + [rootUri + 'foo/baz/tsconfig.json', '{}'], + ]) + ) const updater = new FileSystemUpdater(localfs, memfs) projectManager = new ProjectManager(rootPath, memfs, updater, true) await projectManager.ensureAllFiles().toPromise() }) it('should resolve best configuration based on file name', () => { - const configs = Array.from(projectManager.getChildConfigurations(rootUri + 'foo')).map(config => config.configFilePath) + const configs = Array.from(projectManager.getChildConfigurations(rootUri + 'foo')).map( + config => config.configFilePath + ) assert.deepEqual(configs, [ uri2path(rootUri + 'foo/bar/tsconfig.json'), uri2path(rootUri + 'foo/baz/tsconfig.json'), diff --git a/src/test/tracing.test.ts b/src/test/tracing.test.ts index 9d0590c61..3cccef9b0 100644 --- a/src/test/tracing.test.ts +++ b/src/test/tracing.test.ts @@ -1,4 +1,3 @@ - import * as chai from 'chai' import chaiAsPromised = require('chai-as-promised') import { Span } from 'opentracing' @@ -41,15 +40,17 @@ describe('tracing.ts', () => { let setTagStub: sinon.SinonStub | undefined let logStub: sinon.SinonStub | undefined let finishStub: sinon.SinonStub | undefined - await Promise.resolve(assert.isRejected( - tracePromise('Foo', new Span(), async span => { - setTagStub = sandbox.stub(span, 'setTag') - logStub = sandbox.stub(span, 'log') - finishStub = sandbox.stub(span, 'finish') - throw new Error('Bar') - }), - 'Bar' - )) + await Promise.resolve( + assert.isRejected( + tracePromise('Foo', new Span(), async span => { + setTagStub = sandbox.stub(span, 'setTag') + logStub = sandbox.stub(span, 'log') + finishStub = sandbox.stub(span, 'finish') + throw new Error('Bar') + }), + 'Bar' + ) + ) await new Promise(resolve => setTimeout(resolve, 0)) sinon.assert.calledOnce(setTagStub!) sinon.assert.calledOnce(logStub!) @@ -61,15 +62,17 @@ describe('tracing.ts', () => { let setTagStub: sinon.SinonStub | undefined let logStub: sinon.SinonStub | undefined let finishStub: sinon.SinonStub | undefined - await Promise.resolve(assert.isRejected( - tracePromise('Foo', new Span(), span => { - setTagStub = sandbox.stub(span, 'setTag') - logStub = sandbox.stub(span, 'log') - finishStub = sandbox.stub(span, 'finish') - throw new Error('Bar') - }), - 'Bar' - )) + await Promise.resolve( + assert.isRejected( + tracePromise('Foo', new Span(), span => { + setTagStub = sandbox.stub(span, 'setTag') + logStub = sandbox.stub(span, 'log') + finishStub = sandbox.stub(span, 'finish') + throw new Error('Bar') + }), + 'Bar' + ) + ) await new Promise(resolve => setTimeout(resolve, 0)) sinon.assert.calledOnce(setTagStub!) sinon.assert.calledOnce(logStub!) @@ -83,15 +86,17 @@ describe('tracing.ts', () => { let setTagStub: sinon.SinonStub | undefined let logStub: sinon.SinonStub | undefined let finishStub: sinon.SinonStub | undefined - await Promise.resolve(assert.isRejected( - traceObservable('Foo', new Span(), span => { - setTagStub = sandbox.stub(span, 'setTag') - logStub = sandbox.stub(span, 'log') - finishStub = sandbox.stub(span, 'finish') - return Observable.throw(new Error('Bar')) - }).toPromise(), - 'Bar' - )) + await Promise.resolve( + assert.isRejected( + traceObservable('Foo', new Span(), span => { + setTagStub = sandbox.stub(span, 'setTag') + logStub = sandbox.stub(span, 'log') + finishStub = sandbox.stub(span, 'finish') + return Observable.throw(new Error('Bar')) + }).toPromise(), + 'Bar' + ) + ) await new Promise(resolve => setTimeout(resolve, 0)) sinon.assert.calledOnce(setTagStub!) sinon.assert.calledOnce(logStub!) @@ -103,15 +108,17 @@ describe('tracing.ts', () => { let setTagStub: sinon.SinonStub | undefined let logStub: sinon.SinonStub | undefined let finishStub: sinon.SinonStub | undefined - await Promise.resolve(assert.isRejected( - traceObservable('Foo', new Span(), span => { - setTagStub = sandbox.stub(span, 'setTag') - logStub = sandbox.stub(span, 'log') - finishStub = sandbox.stub(span, 'finish') - throw new Error('Bar') - }).toPromise(), - 'Bar' - )) + await Promise.resolve( + assert.isRejected( + traceObservable('Foo', new Span(), span => { + setTagStub = sandbox.stub(span, 'setTag') + logStub = sandbox.stub(span, 'log') + finishStub = sandbox.stub(span, 'finish') + throw new Error('Bar') + }).toPromise(), + 'Bar' + ) + ) await new Promise(resolve => setTimeout(resolve, 0)) sinon.assert.calledOnce(setTagStub!) sinon.assert.calledOnce(logStub!) diff --git a/src/test/typescript-service-helpers.ts b/src/test/typescript-service-helpers.ts index e0664f331..30b52b8eb 100644 --- a/src/test/typescript-service-helpers.ts +++ b/src/test/typescript-service-helpers.ts @@ -5,10 +5,32 @@ import { IBeforeAndAfterContext, ISuiteCallbackContext, ITestCallbackContext } f import { Observable } from 'rxjs' import * as sinon from 'sinon' import * as ts from 'typescript' -import { CompletionItemKind, CompletionList, DiagnosticSeverity, InsertTextFormat, TextDocumentIdentifier, TextDocumentItem, WorkspaceEdit } from 'vscode-languageserver' -import { Command, Diagnostic, Hover, Location, SignatureHelp, SymbolInformation, SymbolKind } from 'vscode-languageserver-types' +import { + CompletionItemKind, + CompletionList, + DiagnosticSeverity, + InsertTextFormat, + TextDocumentIdentifier, + TextDocumentItem, + WorkspaceEdit, +} from 'vscode-languageserver' +import { + Command, + Diagnostic, + Hover, + Location, + SignatureHelp, + SymbolInformation, + SymbolKind, +} from 'vscode-languageserver-types' import { LanguageClient, RemoteLanguageClient } from '../lang-handler' -import { DependencyReference, PackageInformation, ReferenceInformation, TextDocumentContentParams, WorkspaceFilesParams } from '../request-type' +import { + DependencyReference, + PackageInformation, + ReferenceInformation, + TextDocumentContentParams, + WorkspaceFilesParams, +} from '../request-type' import { ClientCapabilities, CompletionItem, SymbolLocationInformation } from '../request-type' import { TypeScriptService, TypeScriptServiceFactory } from '../typescript-service' import { observableFromIterable, toUnixPath, uri2path } from '../util' @@ -22,7 +44,6 @@ const DEFAULT_CAPABILITIES: ClientCapabilities = { } export interface TestContext { - /** TypeScript service under test */ service: TypeScriptService @@ -41,33 +62,40 @@ export const initializeTypeScriptService = ( rootUri: string, files: Map, clientCapabilities: ClientCapabilities = DEFAULT_CAPABILITIES -) => async function(this: TestContext & IBeforeAndAfterContext): Promise { - - // Stub client - this.client = sinon.createStubInstance(RemoteLanguageClient) - this.client.textDocumentXcontent.callsFake((params: TextDocumentContentParams): Observable => { - if (!files.has(params.textDocument.uri)) { - return Observable.throw(new Error(`Text document ${params.textDocument.uri} does not exist`)) - } - return Observable.of({ - uri: params.textDocument.uri, - text: files.get(params.textDocument.uri)!, - version: 1, - languageId: '', +) => + async function(this: TestContext & IBeforeAndAfterContext): Promise { + // Stub client + this.client = sinon.createStubInstance(RemoteLanguageClient) + this.client.textDocumentXcontent.callsFake((params: TextDocumentContentParams): Observable< + TextDocumentItem + > => { + if (!files.has(params.textDocument.uri)) { + return Observable.throw(new Error(`Text document ${params.textDocument.uri} does not exist`)) + } + return Observable.of({ + uri: params.textDocument.uri, + text: files.get(params.textDocument.uri)!, + version: 1, + languageId: '', + }) }) - }) - this.client.workspaceXfiles.callsFake((params: WorkspaceFilesParams): Observable => - observableFromIterable(files.keys()).map(uri => ({ uri })).toArray()) - this.client.xcacheGet.callsFake(() => Observable.of(null)) - this.client.workspaceApplyEdit.callsFake(() => Observable.of({applied: true})) - this.service = createService(this.client) - - await this.service.initialize({ - processId: process.pid, - rootUri, - capabilities: clientCapabilities || DEFAULT_CAPABILITIES, - }).toPromise() -} + this.client.workspaceXfiles.callsFake((params: WorkspaceFilesParams): Observable => + observableFromIterable(files.keys()) + .map(uri => ({ uri })) + .toArray() + ) + this.client.xcacheGet.callsFake(() => Observable.of(null)) + this.client.workspaceApplyEdit.callsFake(() => Observable.of({ applied: true })) + this.service = createService(this.client) + + await this.service + .initialize({ + processId: process.pid, + rootUri, + capabilities: clientCapabilities || DEFAULT_CAPABILITIES, + }) + .toPromise() + } /** * Shuts the TypeScriptService down (to be used in `afterEach()`) @@ -81,173 +109,205 @@ export async function shutdownTypeScriptService(this: TestContext & IBeforeAndAf * * @param createService Factory function to create the TypeScriptService instance to describe */ -export function describeTypeScriptService(createService: TypeScriptServiceFactory, shutdownService = shutdownTypeScriptService, rootUri: string): void { - +export function describeTypeScriptService( + createService: TypeScriptServiceFactory, + shutdownService = shutdownTypeScriptService, + rootUri: string +): void { describe('Workspace without project files', function(this: TestContext & ISuiteCallbackContext): void { - - beforeEach(initializeTypeScriptService(createService, rootUri, new Map([ - [rootUri + 'a.ts', 'const abc = 1; console.log(abc);'], - [rootUri + 'foo/b.ts', [ - '/* This is class Foo */', - 'export class Foo {}', - ].join('\n')], - [rootUri + 'foo/c.ts', 'import {Foo} from "./b";'], - [rootUri + 'd.ts', [ - 'export interface I {', - ' target: string;', - '}', - ].join('\n')], - [rootUri + 'e.ts', [ - 'import * as d from "./d";', - '', - 'let i: d.I = { target: "hi" };', - 'let target = i.target;', - ].join('\n')], - ]))) + beforeEach( + initializeTypeScriptService( + createService, + rootUri, + new Map([ + [rootUri + 'a.ts', 'const abc = 1; console.log(abc);'], + [rootUri + 'foo/b.ts', ['/* This is class Foo */', 'export class Foo {}'].join('\n')], + [rootUri + 'foo/c.ts', 'import {Foo} from "./b";'], + [rootUri + 'd.ts', ['export interface I {', ' target: string;', '}'].join('\n')], + [ + rootUri + 'e.ts', + [ + 'import * as d from "./d";', + '', + 'let i: d.I = { target: "hi" };', + 'let target = i.target;', + ].join('\n'), + ], + ]) + ) + ) afterEach(shutdownService) describe('textDocumentDefinition()', function(this: TestContext & ISuiteCallbackContext): void { specify('in same file', async function(this: TestContext & ITestCallbackContext): Promise { - const result: Location[] = await this.service.textDocumentDefinition({ - textDocument: { - uri: rootUri + 'a.ts', - }, - position: { - line: 0, - character: 29, - }, - }).reduce(applyReducer, null as any).toPromise() - assert.deepEqual(result, [{ - uri: rootUri + 'a.ts', - range: { - start: { - line: 0, - character: 6, + const result: Location[] = await this.service + .textDocumentDefinition({ + textDocument: { + uri: rootUri + 'a.ts', }, - end: { + position: { line: 0, - character: 9, + character: 29, + }, + }) + .reduce(applyReducer, null as any) + .toPromise() + assert.deepEqual(result, [ + { + uri: rootUri + 'a.ts', + range: { + start: { + line: 0, + character: 6, + }, + end: { + line: 0, + character: 9, + }, }, }, - }]) + ]) }) specify('on keyword (non-null)', async function(this: TestContext & ITestCallbackContext): Promise { - const result: Location[] = await this.service.textDocumentDefinition({ - textDocument: { - uri: rootUri + 'a.ts', - }, - position: { - line: 0, - character: 0, - }, - }).reduce(applyReducer, null as any).toPromise() + const result: Location[] = await this.service + .textDocumentDefinition({ + textDocument: { + uri: rootUri + 'a.ts', + }, + position: { + line: 0, + character: 0, + }, + }) + .reduce(applyReducer, null as any) + .toPromise() assert.deepEqual(result, []) }) specify('in other file', async function(this: TestContext & ITestCallbackContext): Promise { - const result: Location[] = await this.service.textDocumentDefinition({ - textDocument: { - uri: rootUri + 'foo/c.ts', - }, - position: { - line: 0, - character: 9, - }, - }).reduce(applyReducer, null as any).toPromise() - assert.deepEqual(result, [{ - uri: rootUri + 'foo/b.ts', - range: { - start: { - line: 1, - character: 13, + const result: Location[] = await this.service + .textDocumentDefinition({ + textDocument: { + uri: rootUri + 'foo/c.ts', }, - end: { - line: 1, - character: 16, + position: { + line: 0, + character: 9, }, - }, - }]) - }) - }) - describe('textDocumentXdefinition()', function(this: TestContext & ISuiteCallbackContext): void { - specify('on interface field reference', async function(this: TestContext & ITestCallbackContext): Promise { - const result: SymbolLocationInformation[] = await this.service.textDocumentXdefinition({ - textDocument: { - uri: rootUri + 'e.ts', - }, - position: { - line: 3, - character: 15, - }, - }).reduce(applyReducer, null as any).toPromise() - assert.deepEqual(result, [{ - location: { - uri: rootUri + 'd.ts', + }) + .reduce(applyReducer, null as any) + .toPromise() + assert.deepEqual(result, [ + { + uri: rootUri + 'foo/b.ts', range: { start: { line: 1, - character: 2, + character: 13, }, end: { line: 1, - character: 8, + character: 16, }, }, }, - symbol: { - filePath: 'd.ts', - containerName: 'd.I', - containerKind: '', - kind: 'property', - name: 'target', + ]) + }) + }) + describe('textDocumentXdefinition()', function(this: TestContext & ISuiteCallbackContext): void { + specify('on interface field reference', async function( + this: TestContext & ITestCallbackContext + ): Promise { + const result: SymbolLocationInformation[] = await this.service + .textDocumentXdefinition({ + textDocument: { + uri: rootUri + 'e.ts', + }, + position: { + line: 3, + character: 15, + }, + }) + .reduce(applyReducer, null as any) + .toPromise() + assert.deepEqual(result, [ + { + location: { + uri: rootUri + 'd.ts', + range: { + start: { + line: 1, + character: 2, + }, + end: { + line: 1, + character: 8, + }, + }, + }, + symbol: { + filePath: 'd.ts', + containerName: 'd.I', + containerKind: '', + kind: 'property', + name: 'target', + }, }, - }]) + ]) }) specify('in same file', async function(this: TestContext & ITestCallbackContext): Promise { - const result: SymbolLocationInformation[] = await this.service.textDocumentXdefinition({ - textDocument: { - uri: rootUri + 'a.ts', - }, - position: { - line: 0, - character: 29, - }, - }).reduce(applyReducer, null as any).toPromise() - assert.deepEqual(result, [{ - location: { - uri: rootUri + 'a.ts', - range: { - start: { - line: 0, - character: 6, - }, - end: { - line: 0, - character: 9, + const result: SymbolLocationInformation[] = await this.service + .textDocumentXdefinition({ + textDocument: { + uri: rootUri + 'a.ts', + }, + position: { + line: 0, + character: 29, + }, + }) + .reduce(applyReducer, null as any) + .toPromise() + assert.deepEqual(result, [ + { + location: { + uri: rootUri + 'a.ts', + range: { + start: { + line: 0, + character: 6, + }, + end: { + line: 0, + character: 9, + }, }, }, + symbol: { + filePath: 'a.ts', + containerName: '"a"', + containerKind: 'module', + kind: 'const', + name: 'abc', + }, }, - symbol: { - filePath: 'a.ts', - containerName: '"a"', - containerKind: 'module', - kind: 'const', - name: 'abc', - }, - }]) + ]) }) }) describe('textDocumentHover()', function(this: TestContext & ISuiteCallbackContext): void { specify('in same file', async function(this: TestContext & ITestCallbackContext): Promise { - const result: Hover = await this.service.textDocumentHover({ - textDocument: { - uri: rootUri + 'a.ts', - }, - position: { - line: 0, - character: 29, - }, - }).reduce(applyReducer, null as any).toPromise() + const result: Hover = await this.service + .textDocumentHover({ + textDocument: { + uri: rootUri + 'a.ts', + }, + position: { + line: 0, + character: 29, + }, + }) + .reduce(applyReducer, null as any) + .toPromise() assert.deepEqual(result, { range: { start: { @@ -259,22 +319,22 @@ export function describeTypeScriptService(createService: TypeScriptServiceFactor character: 30, }, }, - contents: [ - { language: 'typescript', value: 'const abc: 1' }, - '**const**', - ], + contents: [{ language: 'typescript', value: 'const abc: 1' }, '**const**'], }) }) specify('in other file', async function(this: TestContext & ITestCallbackContext): Promise { - const result: Hover = await this.service.textDocumentHover({ - textDocument: { - uri: rootUri + 'foo/c.ts', - }, - position: { - line: 0, - character: 9, - }, - }).reduce(applyReducer, null as any).toPromise() + const result: Hover = await this.service + .textDocumentHover({ + textDocument: { + uri: rootUri + 'foo/c.ts', + }, + position: { + line: 0, + character: 9, + }, + }) + .reduce(applyReducer, null as any) + .toPromise() assert.deepEqual(result, { range: { end: { @@ -286,98 +346,91 @@ export function describeTypeScriptService(createService: TypeScriptServiceFactor character: 8, }, }, - contents: [ - { language: 'typescript', value: 'import Foo' }, - '**alias**', - ], + contents: [{ language: 'typescript', value: 'import Foo' }, '**alias**'], }) }) specify('over keyword (non-null)', async function(this: TestContext & ITestCallbackContext): Promise { - const result: Hover = await this.service.textDocumentHover({ - textDocument: { - uri: rootUri + 'a.ts', - }, - position: { - line: 0, - character: 0, - }, - }).reduce(applyReducer, null as any).toPromise() + const result: Hover = await this.service + .textDocumentHover({ + textDocument: { + uri: rootUri + 'a.ts', + }, + position: { + line: 0, + character: 0, + }, + }) + .reduce(applyReducer, null as any) + .toPromise() assert.deepEqual(result, { contents: [] }) }) specify('over non-existent file', async function(this: TestContext & ITestCallbackContext): Promise { - await Promise.resolve(assert.isRejected(this.service.textDocumentHover({ - textDocument: { - uri: rootUri + 'foo/a.ts', - }, - position: { - line: 0, - character: 0, - }, - }).toPromise())) + await Promise.resolve( + assert.isRejected( + this.service + .textDocumentHover({ + textDocument: { + uri: rootUri + 'foo/a.ts', + }, + position: { + line: 0, + character: 0, + }, + }) + .toPromise() + ) + ) }) }) }) describe('Workspace with typings directory', function(this: TestContext & ISuiteCallbackContext): void { - - beforeEach(initializeTypeScriptService(createService, rootUri, new Map([ - [rootUri + 'src/a.ts', 'import * as m from \'dep\';'], - [rootUri + 'typings/dep.d.ts', 'declare module \'dep\' {}'], - [rootUri + 'src/tsconfig.json', [ - '{', - ' "compilerOptions": {', - ' "target": "ES5",', - ' "module": "commonjs",', - ' "sourceMap": true,', - ' "noImplicitAny": false,', - ' "removeComments": false,', - ' "preserveConstEnums": true', - ' }', - '}', - ].join('\n')], - [rootUri + 'src/tsd.d.ts', '/// '], - [rootUri + 'src/dir/index.ts', 'import * as m from "dep";'], - ]))) + beforeEach( + initializeTypeScriptService( + createService, + rootUri, + new Map([ + [rootUri + 'src/a.ts', "import * as m from 'dep';"], + [rootUri + 'typings/dep.d.ts', "declare module 'dep' {}"], + [ + rootUri + 'src/tsconfig.json', + [ + '{', + ' "compilerOptions": {', + ' "target": "ES5",', + ' "module": "commonjs",', + ' "sourceMap": true,', + ' "noImplicitAny": false,', + ' "removeComments": false,', + ' "preserveConstEnums": true', + ' }', + '}', + ].join('\n'), + ], + [rootUri + 'src/tsd.d.ts', '/// '], + [rootUri + 'src/dir/index.ts', 'import * as m from "dep";'], + ]) + ) + ) afterEach(shutdownService) describe('textDocumentDefinition()', function(this: TestContext & ISuiteCallbackContext): void { specify('with tsd.d.ts', async function(this: TestContext & ITestCallbackContext): Promise { - const result: Location[] = await this.service.textDocumentDefinition({ - textDocument: { - uri: rootUri + 'src/dir/index.ts', - }, - position: { - line: 0, - character: 20, - }, - }).reduce(applyReducer, null as any).toPromise() - assert.deepEqual(result, [{ - uri: rootUri + 'typings/dep.d.ts', - range: { - start: { - line: 0, - character: 15, - }, - end: { - line: 0, - character: 20, - }, - }, - }]) - }) - describe('on file in project root', function(this: TestContext & ISuiteCallbackContext): void { - specify('on import alias', async function(this: TestContext & ITestCallbackContext): Promise { - const result: Location[] = await this.service.textDocumentDefinition({ + const result: Location[] = await this.service + .textDocumentDefinition({ textDocument: { - uri: rootUri + 'src/a.ts', + uri: rootUri + 'src/dir/index.ts', }, position: { line: 0, - character: 12, + character: 20, }, - }).reduce(applyReducer, null as any).toPromise() - assert.deepEqual(result, [{ + }) + .reduce(applyReducer, null as any) + .toPromise() + assert.deepEqual(result, [ + { uri: rootUri + 'typings/dep.d.ts', range: { start: { @@ -389,158 +442,220 @@ export function describeTypeScriptService(createService: TypeScriptServiceFactor character: 20, }, }, - }]) + }, + ]) + }) + describe('on file in project root', function(this: TestContext & ISuiteCallbackContext): void { + specify('on import alias', async function(this: TestContext & ITestCallbackContext): Promise { + const result: Location[] = await this.service + .textDocumentDefinition({ + textDocument: { + uri: rootUri + 'src/a.ts', + }, + position: { + line: 0, + character: 12, + }, + }) + .reduce(applyReducer, null as any) + .toPromise() + assert.deepEqual(result, [ + { + uri: rootUri + 'typings/dep.d.ts', + range: { + start: { + line: 0, + character: 15, + }, + end: { + line: 0, + character: 20, + }, + }, + }, + ]) }) specify('on module name', async function(this: TestContext & ITestCallbackContext): Promise { - const result: Location[] = await this.service.textDocumentDefinition({ - textDocument: { - uri: rootUri + 'src/a.ts', - }, - position: { - line: 0, - character: 20, - }, - }).reduce(applyReducer, null as any).toPromise() - assert.deepEqual(result, [{ - uri: rootUri + 'typings/dep.d.ts', - range: { - start: { - line: 0, - character: 15, + const result: Location[] = await this.service + .textDocumentDefinition({ + textDocument: { + uri: rootUri + 'src/a.ts', }, - end: { + position: { line: 0, character: 20, }, + }) + .reduce(applyReducer, null as any) + .toPromise() + assert.deepEqual(result, [ + { + uri: rootUri + 'typings/dep.d.ts', + range: { + start: { + line: 0, + character: 15, + }, + end: { + line: 0, + character: 20, + }, + }, }, - }]) + ]) }) }) }) }) describe('DefinitelyTyped', function(this: TestContext & ISuiteCallbackContext): void { - beforeEach(initializeTypeScriptService(createService, rootUri, new Map([ - [rootUri + 'package.json', JSON.stringify({ - private: true, - name: 'definitely-typed', - version: '0.0.1', - homepage: 'https://github.com/DefinitelyTyped/DefinitelyTyped', - repository: { - type: 'git', - url: 'git+https://github.com/DefinitelyTyped/DefinitelyTyped.git', - }, - license: 'MIT', - bugs: { - url: 'https://github.com/DefinitelyTyped/DefinitelyTyped/issues', - }, - engines: { - node: '>= 6.9.1', - }, - scripts: { - 'compile-scripts': 'tsc -p scripts', - 'new-package': 'node scripts/new-package.js', - 'not-needed': 'node scripts/not-needed.js', - 'lint': 'node scripts/lint.js', - 'test': 'node node_modules/types-publisher/bin/tester/test.js --run-from-definitely-typed --nProcesses 1', - }, - devDependencies: { - 'types-publisher': 'Microsoft/types-publisher#production', - }, - }, null, 4)], - [rootUri + 'types/resolve/index.d.ts', [ - '/// ', - '', - 'type resolveCallback = (err: Error, resolved?: string) => void;', - 'declare function resolve(id: string, cb: resolveCallback): void;', - '', - ].join('\n')], - [rootUri + 'types/resolve/tsconfig.json', JSON.stringify({ - compilerOptions: { - module: 'commonjs', - lib: [ - 'es6', + beforeEach( + initializeTypeScriptService( + createService, + rootUri, + new Map([ + [ + rootUri + 'package.json', + JSON.stringify( + { + private: true, + name: 'definitely-typed', + version: '0.0.1', + homepage: 'https://github.com/DefinitelyTyped/DefinitelyTyped', + repository: { + type: 'git', + url: 'git+https://github.com/DefinitelyTyped/DefinitelyTyped.git', + }, + license: 'MIT', + bugs: { + url: 'https://github.com/DefinitelyTyped/DefinitelyTyped/issues', + }, + engines: { + node: '>= 6.9.1', + }, + scripts: { + 'compile-scripts': 'tsc -p scripts', + 'new-package': 'node scripts/new-package.js', + 'not-needed': 'node scripts/not-needed.js', + lint: 'node scripts/lint.js', + test: + 'node node_modules/types-publisher/bin/tester/test.js --run-from-definitely-typed --nProcesses 1', + }, + devDependencies: { + 'types-publisher': 'Microsoft/types-publisher#production', + }, + }, + null, + 4 + ), ], - noImplicitAny: true, - noImplicitThis: true, - strictNullChecks: false, - baseUrl: '../', - typeRoots: [ - '../', + [ + rootUri + 'types/resolve/index.d.ts', + [ + '/// ', + '', + 'type resolveCallback = (err: Error, resolved?: string) => void;', + 'declare function resolve(id: string, cb: resolveCallback): void;', + '', + ].join('\n'), ], - types: [], - noEmit: true, - forceConsistentCasingInFileNames: true, - }, - files: [ - 'index.d.ts', - ], - })], - [rootUri + 'types/notResolve/index.d.ts', [ - '/// ', - '', - 'type resolveCallback = (err: Error, resolved?: string) => void;', - 'declare function resolve(id: string, cb: resolveCallback): void;', - '', - ].join('\n')], - [rootUri + 'types/notResolve/tsconfig.json', JSON.stringify({ - compilerOptions: { - module: 'commonjs', - lib: [ - 'es6', + [ + rootUri + 'types/resolve/tsconfig.json', + JSON.stringify({ + compilerOptions: { + module: 'commonjs', + lib: ['es6'], + noImplicitAny: true, + noImplicitThis: true, + strictNullChecks: false, + baseUrl: '../', + typeRoots: ['../'], + types: [], + noEmit: true, + forceConsistentCasingInFileNames: true, + }, + files: ['index.d.ts'], + }), ], - noImplicitAny: true, - noImplicitThis: true, - strictNullChecks: false, - baseUrl: '../', - typeRoots: [ - '../', + [ + rootUri + 'types/notResolve/index.d.ts', + [ + '/// ', + '', + 'type resolveCallback = (err: Error, resolved?: string) => void;', + 'declare function resolve(id: string, cb: resolveCallback): void;', + '', + ].join('\n'), ], - types: [], - noEmit: true, - forceConsistentCasingInFileNames: true, - }, - files: [ - 'index.d.ts', - ], - })], - ]))) + [ + rootUri + 'types/notResolve/tsconfig.json', + JSON.stringify({ + compilerOptions: { + module: 'commonjs', + lib: ['es6'], + noImplicitAny: true, + noImplicitThis: true, + strictNullChecks: false, + baseUrl: '../', + typeRoots: ['../'], + types: [], + noEmit: true, + forceConsistentCasingInFileNames: true, + }, + files: ['index.d.ts'], + }), + ], + ]) + ) + ) afterEach(shutdownService) describe('workspaceSymbol()', function(this: TestContext & ISuiteCallbackContext): void { - it('should find a symbol by SymbolDescriptor query with name and package name', async function(this: TestContext & ITestCallbackContext): Promise { - const result: SymbolInformation[] = await this.service.workspaceSymbol({ - symbol: { name: 'resolveCallback', package: { name: '@types/resolve' } }, - }).reduce(applyReducer, null as any).toPromise() - assert.deepEqual(result, [{ - kind: SymbolKind.Variable, - location: { - range: { - end: { - character: 63, - line: 2, - }, - start: { - character: 0, - line: 2, + it('should find a symbol by SymbolDescriptor query with name and package name', async function( + this: TestContext & ITestCallbackContext + ): Promise { + const result: SymbolInformation[] = await this.service + .workspaceSymbol({ + symbol: { name: 'resolveCallback', package: { name: '@types/resolve' } }, + }) + .reduce(applyReducer, null as any) + .toPromise() + assert.deepEqual(result, [ + { + kind: SymbolKind.Variable, + location: { + range: { + end: { + character: 63, + line: 2, + }, + start: { + character: 0, + line: 2, + }, }, + uri: rootUri + 'types/resolve/index.d.ts', }, - uri: rootUri + 'types/resolve/index.d.ts', + name: 'resolveCallback', }, - name: 'resolveCallback', - }]) + ]) }) - it('should find a symbol by SymbolDescriptor query with name, containerKind and package name', async function(this: TestContext & ITestCallbackContext): Promise { - const result: SymbolInformation[] = await this.service.workspaceSymbol({ - symbol: { - name: 'resolveCallback', - containerKind: 'module', - package: { - name: '@types/resolve', + it('should find a symbol by SymbolDescriptor query with name, containerKind and package name', async function( + this: TestContext & ITestCallbackContext + ): Promise { + const result: SymbolInformation[] = await this.service + .workspaceSymbol({ + symbol: { + name: 'resolveCallback', + containerKind: 'module', + package: { + name: '@types/resolve', + }, }, - }, - }).reduce(applyReducer, null as any).toPromise() + }) + .reduce(applyReducer, null as any) + .toPromise() assert.deepEqual(result[0], { kind: SymbolKind.Variable, location: { @@ -563,32 +678,45 @@ export function describeTypeScriptService(createService: TypeScriptServiceFactor }) describe('Workspace with root package.json', function(this: TestContext & ISuiteCallbackContext): void { - - beforeEach(initializeTypeScriptService(createService, rootUri, new Map([ - [rootUri + 'a.ts', 'class a { foo() { const i = 1;} }'], - [rootUri + 'foo/b.ts', 'class b { bar: number; baz(): number { return this.bar;}}; function qux() {}'], - [rootUri + 'c.ts', 'import { x } from "dep/dep";'], - [rootUri + 'package.json', JSON.stringify({ name: 'mypkg' })], - [rootUri + 'node_modules/dep/dep.ts', 'export var x = 1;'], - [rootUri + 'node_modules/dep/package.json', JSON.stringify({ name: 'dep' })], - ]))) + beforeEach( + initializeTypeScriptService( + createService, + rootUri, + new Map([ + [rootUri + 'a.ts', 'class a { foo() { const i = 1;} }'], + [ + rootUri + 'foo/b.ts', + 'class b { bar: number; baz(): number { return this.bar;}}; function qux() {}', + ], + [rootUri + 'c.ts', 'import { x } from "dep/dep";'], + [rootUri + 'package.json', JSON.stringify({ name: 'mypkg' })], + [rootUri + 'node_modules/dep/dep.ts', 'export var x = 1;'], + [rootUri + 'node_modules/dep/package.json', JSON.stringify({ name: 'dep' })], + ]) + ) + ) afterEach(shutdownService) describe('workspaceSymbol()', function(this: TestContext & ISuiteCallbackContext): void { describe('with SymbolDescriptor query', function(this: TestContext & ISuiteCallbackContext): void { - it('should find a symbol by name, kind and package name', async function(this: TestContext & ITestCallbackContext): Promise { - const result: SymbolInformation[] = await this.service.workspaceSymbol({ - symbol: { - name: 'a', - kind: 'class', - package: { - name: 'mypkg', + it('should find a symbol by name, kind and package name', async function( + this: TestContext & ITestCallbackContext + ): Promise { + const result: SymbolInformation[] = await this.service + .workspaceSymbol({ + symbol: { + name: 'a', + kind: 'class', + package: { + name: 'mypkg', + }, }, - }, - }).reduce(applyReducer, null as any).toPromise() + }) + .reduce(applyReducer, null as any) + .toPromise() assert.deepEqual(result[0], { - kind: SymbolKind.Class, + kind: SymbolKind.Class, location: { range: { end: { @@ -605,10 +733,15 @@ export function describeTypeScriptService(createService: TypeScriptServiceFactor name: 'a', }) }) - it('should find a symbol by name, kind, package name and ignore package version', async function(this: TestContext & ITestCallbackContext): Promise { - const result: SymbolInformation[] = await this.service.workspaceSymbol({ - symbol: { name: 'a', kind: 'class', package: { name: 'mypkg', version: '203940234' } }, - }).reduce(applyReducer, null as any).toPromise() + it('should find a symbol by name, kind, package name and ignore package version', async function( + this: TestContext & ITestCallbackContext + ): Promise { + const result: SymbolInformation[] = await this.service + .workspaceSymbol({ + symbol: { name: 'a', kind: 'class', package: { name: 'mypkg', version: '203940234' } }, + }) + .reduce(applyReducer, null as any) + .toPromise() assert.deepEqual(result[0], { kind: SymbolKind.Class, location: { @@ -627,68 +760,86 @@ export function describeTypeScriptService(createService: TypeScriptServiceFactor name: 'a', }) }) - it('should find a symbol by name', async function(this: TestContext & ITestCallbackContext): Promise { - const result: SymbolInformation[] = await this.service.workspaceSymbol({ - symbol: { - name: 'a', - }, - }).reduce(applyReducer, null as any).toPromise() - assert.deepEqual(result, [{ - kind: SymbolKind.Class, - location: { - range: { - end: { - character: 33, - line: 0, - }, - start: { - character: 0, - line: 0, + it('should find a symbol by name', async function( + this: TestContext & ITestCallbackContext + ): Promise { + const result: SymbolInformation[] = await this.service + .workspaceSymbol({ + symbol: { + name: 'a', + }, + }) + .reduce(applyReducer, null as any) + .toPromise() + assert.deepEqual(result, [ + { + kind: SymbolKind.Class, + location: { + range: { + end: { + character: 33, + line: 0, + }, + start: { + character: 0, + line: 0, + }, }, + uri: rootUri + 'a.ts', }, - uri: rootUri + 'a.ts', + name: 'a', }, - name: 'a', - }]) + ]) }) - it('should return no result if the PackageDescriptor does not match', async function(this: TestContext & ITestCallbackContext): Promise { - const result: SymbolInformation[] = await this.service.workspaceSymbol({ - symbol: { - name: 'a', - kind: 'class', - package: { - name: 'not-mypkg', + it('should return no result if the PackageDescriptor does not match', async function( + this: TestContext & ITestCallbackContext + ): Promise { + const result: SymbolInformation[] = await this.service + .workspaceSymbol({ + symbol: { + name: 'a', + kind: 'class', + package: { + name: 'not-mypkg', + }, }, - }, - }).reduce(applyReducer, null as any).toPromise() + }) + .reduce(applyReducer, null as any) + .toPromise() assert.deepEqual(result, []) }) }) describe('with text query', function(this: TestContext & ISuiteCallbackContext): void { it('should find a symbol', async function(this: TestContext & ITestCallbackContext): Promise { - const result: SymbolInformation[] = await this.service.workspaceSymbol({ query: 'a' }) + const result: SymbolInformation[] = await this.service + .workspaceSymbol({ query: 'a' }) .reduce(applyReducer, null as any) .toPromise() - assert.deepEqual(result, [{ - kind: SymbolKind.Class, - location: { - range: { - end: { - character: 33, - line: 0, - }, - start: { - character: 0, - line: 0, + assert.deepEqual(result, [ + { + kind: SymbolKind.Class, + location: { + range: { + end: { + character: 33, + line: 0, + }, + start: { + character: 0, + line: 0, + }, }, + uri: rootUri + 'a.ts', }, - uri: rootUri + 'a.ts', + name: 'a', }, - name: 'a', - }]) + ]) }) - it('should return all symbols for an empty query excluding dependencies', async function(this: TestContext & ITestCallbackContext): Promise { - const result: SymbolInformation[] = await this.service.workspaceSymbol({ query: '' }) + it('should return all symbols for an empty query excluding dependencies', async function( + this: TestContext & ITestCallbackContext + ): Promise { + const result: SymbolInformation[] = await this.service + .workspaceSymbol({ query: '' }) .reduce(applyReducer, null as any) .toPromise() assert.deepEqual(result, [ @@ -856,35 +1007,43 @@ export function describeTypeScriptService(createService: TypeScriptServiceFactor }) describe('workspaceXreferences()', function(this: TestContext & ISuiteCallbackContext): void { - it('should return all references to a method', async function(this: TestContext & ITestCallbackContext): Promise { - const result: ReferenceInformation[] = await this.service.workspaceXreferences({ query: { name: 'foo', kind: 'method', containerName: 'a' } }) + it('should return all references to a method', async function( + this: TestContext & ITestCallbackContext + ): Promise { + const result: ReferenceInformation[] = await this.service + .workspaceXreferences({ query: { name: 'foo', kind: 'method', containerName: 'a' } }) .reduce(applyReducer, null as any) .toPromise() - assert.deepEqual(result, [{ - symbol: { - filePath: 'a.ts', - containerKind: '', - containerName: 'a', - name: 'foo', - kind: 'method', - }, - reference: { - range: { - end: { - character: 13, - line: 0, - }, - start: { - character: 9, - line: 0, + assert.deepEqual(result, [ + { + symbol: { + filePath: 'a.ts', + containerKind: '', + containerName: 'a', + name: 'foo', + kind: 'method', + }, + reference: { + range: { + end: { + character: 13, + line: 0, + }, + start: { + character: 9, + line: 0, + }, }, + uri: rootUri + 'a.ts', }, - uri: rootUri + 'a.ts', }, - }]) + ]) }) - it('should return all references to a method with hinted dependee package name', async function(this: TestContext & ITestCallbackContext): Promise { - const result: ReferenceInformation[] = await this.service.workspaceXreferences({ + it('should return all references to a method with hinted dependee package name', async function( + this: TestContext & ITestCallbackContext + ): Promise { + const result: ReferenceInformation[] = await this.service + .workspaceXreferences({ query: { name: 'foo', kind: 'method', @@ -896,105 +1055,123 @@ export function describeTypeScriptService(createService: TypeScriptServiceFactor }) .reduce(applyReducer, null as any) .toPromise() - assert.deepEqual(result, [{ - symbol: { - filePath: 'a.ts', - containerKind: '', - containerName: 'a', - name: 'foo', - kind: 'method', - }, - reference: { - range: { - end: { - character: 13, - line: 0, - }, - start: { - character: 9, - line: 0, + assert.deepEqual(result, [ + { + symbol: { + filePath: 'a.ts', + containerKind: '', + containerName: 'a', + name: 'foo', + kind: 'method', + }, + reference: { + range: { + end: { + character: 13, + line: 0, + }, + start: { + character: 9, + line: 0, + }, }, + uri: rootUri + 'a.ts', }, - uri: rootUri + 'a.ts', }, - }]) + ]) }) - it('should return no references to a method if hinted dependee package name was not found', async function(this: TestContext & ITestCallbackContext): Promise { - const result = await this.service.workspaceXreferences({ - query: { - name: 'foo', - kind: 'method', - containerName: 'a', - }, - hints: { - dependeePackageName: 'NOT-mypkg', - }, - }) + it('should return no references to a method if hinted dependee package name was not found', async function( + this: TestContext & ITestCallbackContext + ): Promise { + const result = await this.service + .workspaceXreferences({ + query: { + name: 'foo', + kind: 'method', + containerName: 'a', + }, + hints: { + dependeePackageName: 'NOT-mypkg', + }, + }) .reduce(applyReducer, null as any) .toPromise() assert.deepEqual(result, []) }) - it('should return all references to a symbol from a dependency', async function(this: TestContext & ITestCallbackContext): Promise { - const result: ReferenceInformation[] = await this.service.workspaceXreferences({ query: { name: 'x' } }) + it('should return all references to a symbol from a dependency', async function( + this: TestContext & ITestCallbackContext + ): Promise { + const result: ReferenceInformation[] = await this.service + .workspaceXreferences({ query: { name: 'x' } }) .reduce(applyReducer, null as any) .toPromise() - assert.deepEqual(result, [{ - reference: { - range: { - end: { - character: 10, - line: 0, - }, - start: { - character: 8, - line: 0, + assert.deepEqual(result, [ + { + reference: { + range: { + end: { + character: 10, + line: 0, + }, + start: { + character: 8, + line: 0, + }, }, + uri: rootUri + 'c.ts', + }, + symbol: { + filePath: 'dep/dep.ts', + containerKind: '', + containerName: '"dep/dep"', + kind: 'var', + name: 'x', }, - uri: rootUri + 'c.ts', - }, - symbol: { - filePath: 'dep/dep.ts', - containerKind: '', - containerName: '"dep/dep"', - kind: 'var', - name: 'x', }, - }]) + ]) }) - it('should return all references to a symbol from a dependency with PackageDescriptor query', async function(this: TestContext & ITestCallbackContext): Promise { - const result: ReferenceInformation[] = await this.service.workspaceXreferences({ query: { name: 'x', package: { name: 'dep' } } }) + it('should return all references to a symbol from a dependency with PackageDescriptor query', async function( + this: TestContext & ITestCallbackContext + ): Promise { + const result: ReferenceInformation[] = await this.service + .workspaceXreferences({ query: { name: 'x', package: { name: 'dep' } } }) .reduce(applyReducer, null as any) .toPromise() - assert.deepEqual(result, [{ - reference: { - range: { - end: { - character: 10, - line: 0, - }, - start: { - character: 8, - line: 0, + assert.deepEqual(result, [ + { + reference: { + range: { + end: { + character: 10, + line: 0, + }, + start: { + character: 8, + line: 0, + }, }, + uri: rootUri + 'c.ts', }, - uri: rootUri + 'c.ts', - }, - symbol: { - filePath: 'dep/dep.ts', - containerKind: '', - containerName: '"dep/dep"', - kind: 'var', - name: 'x', - package: { - name: 'dep', - repoURL: undefined, - version: undefined, + symbol: { + filePath: 'dep/dep.ts', + containerKind: '', + containerName: '"dep/dep"', + kind: 'var', + name: 'x', + package: { + name: 'dep', + repoURL: undefined, + version: undefined, + }, }, }, - }]) + ]) }) - it('should return all references to all symbols if empty SymbolDescriptor query is passed', async function(this: TestContext & ITestCallbackContext): Promise { - const result: ReferenceInformation[] = await this.service.workspaceXreferences({ query: {} }) + it('should return all references to all symbols if empty SymbolDescriptor query is passed', async function( + this: TestContext & ITestCallbackContext + ): Promise { + const result: ReferenceInformation[] = await this.service + .workspaceXreferences({ query: {} }) .reduce(applyReducer, null as any) .toPromise() assert.deepEqual(result, [ @@ -1202,49 +1379,66 @@ export function describeTypeScriptService(createService: TypeScriptServiceFactor }) describe('Dependency detection', function(this: TestContext & ISuiteCallbackContext): void { - - beforeEach(initializeTypeScriptService(createService, rootUri, new Map([ - [rootUri + 'package.json', JSON.stringify({ - name: 'tslint', - version: '4.0.2', - dependencies: { - 'babel-code-frame': '^6.16.0', - 'findup-sync': '~0.3.0', - }, - devDependencies: { - '@types/babel-code-frame': '^6.16.0', - '@types/optimist': '0.0.29', - 'chai': '^3.0.0', - 'tslint': 'latest', - 'tslint-test-config-non-relative': 'file:test/external/tslint-test-config-non-relative', - 'typescript': '2.0.10', - }, - peerDependencies: { - typescript: '>=2.0.0', - }, - })], - [rootUri + 'node_modules/dep/package.json', JSON.stringify({ - name: 'foo', - dependencies: { - shouldnotinclude: '0.0.0', - }, - })], - [rootUri + 'subproject/package.json', JSON.stringify({ - name: 'subproject', - repository: { - url: 'https://github.com/my/subproject', - }, - dependencies: { - 'subproject-dep': '0.0.0', - }, - })], - ]))) + beforeEach( + initializeTypeScriptService( + createService, + rootUri, + new Map([ + [ + rootUri + 'package.json', + JSON.stringify({ + name: 'tslint', + version: '4.0.2', + dependencies: { + 'babel-code-frame': '^6.16.0', + 'findup-sync': '~0.3.0', + }, + devDependencies: { + '@types/babel-code-frame': '^6.16.0', + '@types/optimist': '0.0.29', + chai: '^3.0.0', + tslint: 'latest', + 'tslint-test-config-non-relative': 'file:test/external/tslint-test-config-non-relative', + typescript: '2.0.10', + }, + peerDependencies: { + typescript: '>=2.0.0', + }, + }), + ], + [ + rootUri + 'node_modules/dep/package.json', + JSON.stringify({ + name: 'foo', + dependencies: { + shouldnotinclude: '0.0.0', + }, + }), + ], + [ + rootUri + 'subproject/package.json', + JSON.stringify({ + name: 'subproject', + repository: { + url: 'https://github.com/my/subproject', + }, + dependencies: { + 'subproject-dep': '0.0.0', + }, + }), + ], + ]) + ) + ) afterEach(shutdownService) describe('workspaceXdependencies()', function(this: TestContext & ISuiteCallbackContext): void { - it('should account for all dependencies', async function(this: TestContext & ITestCallbackContext): Promise { - const result: DependencyReference[] = await this.service.workspaceXdependencies() + it('should account for all dependencies', async function( + this: TestContext & ITestCallbackContext + ): Promise { + const result: DependencyReference[] = await this.service + .workspaceXdependencies() .reduce(applyReducer, null as any) .toPromise() assert.deepEqual(result, [ @@ -1342,130 +1536,144 @@ export function describeTypeScriptService(createService: TypeScriptServiceFactor }) }) describe('workspaceXpackages()', function(this: TestContext & ISuiteCallbackContext): void { - it('should accournt for all packages', async function(this: TestContext & ITestCallbackContext): Promise { - const result: PackageInformation[] = await this.service.workspaceXpackages() + it('should accournt for all packages', async function( + this: TestContext & ITestCallbackContext + ): Promise { + const result: PackageInformation[] = await this.service + .workspaceXpackages() .reduce(applyReducer, null as any) .toPromise() - assert.deepEqual(result, [{ - package: { - name: 'tslint', - version: '4.0.2', - repoURL: undefined, - }, - dependencies: [ - { - attributes: { - name: 'babel-code-frame', - version: '^6.16.0', - }, - hints: { - dependeePackageName: 'tslint', - }, - }, - { - attributes: { - name: 'findup-sync', - version: '~0.3.0', - }, - hints: { - dependeePackageName: 'tslint', - }, - }, - { - attributes: { - name: '@types/babel-code-frame', - version: '^6.16.0', - }, - hints: { - dependeePackageName: 'tslint', - }, + assert.deepEqual(result, [ + { + package: { + name: 'tslint', + version: '4.0.2', + repoURL: undefined, }, - { - attributes: { - name: '@types/optimist', - version: '0.0.29', - }, - hints: { - dependeePackageName: 'tslint', + dependencies: [ + { + attributes: { + name: 'babel-code-frame', + version: '^6.16.0', + }, + hints: { + dependeePackageName: 'tslint', + }, }, - }, - { - attributes: { - name: 'chai', - version: '^3.0.0', + { + attributes: { + name: 'findup-sync', + version: '~0.3.0', + }, + hints: { + dependeePackageName: 'tslint', + }, }, - hints: { - dependeePackageName: 'tslint', + { + attributes: { + name: '@types/babel-code-frame', + version: '^6.16.0', + }, + hints: { + dependeePackageName: 'tslint', + }, }, - }, - { - attributes: { - name: 'tslint', - version: 'latest', + { + attributes: { + name: '@types/optimist', + version: '0.0.29', + }, + hints: { + dependeePackageName: 'tslint', + }, }, - hints: { - dependeePackageName: 'tslint', + { + attributes: { + name: 'chai', + version: '^3.0.0', + }, + hints: { + dependeePackageName: 'tslint', + }, }, - }, - { - attributes: { - name: 'tslint-test-config-non-relative', - version: 'file:test/external/tslint-test-config-non-relative', + { + attributes: { + name: 'tslint', + version: 'latest', + }, + hints: { + dependeePackageName: 'tslint', + }, }, - hints: { - dependeePackageName: 'tslint', + { + attributes: { + name: 'tslint-test-config-non-relative', + version: 'file:test/external/tslint-test-config-non-relative', + }, + hints: { + dependeePackageName: 'tslint', + }, }, - }, - { - attributes: { - name: 'typescript', - version: '2.0.10', + { + attributes: { + name: 'typescript', + version: '2.0.10', + }, + hints: { + dependeePackageName: 'tslint', + }, }, - hints: { - dependeePackageName: 'tslint', + { + attributes: { + name: 'typescript', + version: '>=2.0.0', + }, + hints: { + dependeePackageName: 'tslint', + }, }, + ], + }, + { + package: { + name: 'subproject', + version: undefined, + repoURL: 'https://github.com/my/subproject', }, - { - attributes: { - name: 'typescript', - version: '>=2.0.0', + dependencies: [ + { + attributes: { name: 'subproject-dep', version: '0.0.0' }, + hints: { dependeePackageName: 'subproject' }, }, - hints: { - dependeePackageName: 'tslint', - }, - }, - ], - }, { - package: { - name: 'subproject', - version: undefined, - repoURL: 'https://github.com/my/subproject', - }, - dependencies: [ - { attributes: { name: 'subproject-dep', version: '0.0.0' }, hints: { dependeePackageName: 'subproject' } }, - ], - }]) + ], + }, + ]) }) }) }) describe('TypeScript library', function(this: TestContext & ISuiteCallbackContext): void { - beforeEach(initializeTypeScriptService(createService, rootUri, new Map([ - [rootUri + 'a.ts', 'let parameters = [];'], - ]))) + beforeEach( + initializeTypeScriptService(createService, rootUri, new Map([[rootUri + 'a.ts', 'let parameters = [];']])) + ) afterEach(shutdownService) - specify('type of parameters should be any[]', async function(this: TestContext & ITestCallbackContext): Promise { - const result: Hover = await this.service.textDocumentHover({ - textDocument: { - uri: rootUri + 'a.ts', - }, - position: { - line: 0, - character: 5, - }, - }).reduce(applyReducer, null as any).toPromise() + specify('type of parameters should be any[]', async function( + this: TestContext & ITestCallbackContext + ): Promise { + const result: Hover = await this.service + .textDocumentHover({ + textDocument: { + uri: rootUri + 'a.ts', + }, + position: { + line: 0, + character: 5, + }, + }) + .reduce(applyReducer, null as any) + .toPromise() assert.deepEqual(result, { range: { end: { @@ -1477,24 +1685,21 @@ export function describeTypeScriptService(createService: TypeScriptServiceFactor line: 0, }, }, - contents: [ - { language: 'typescript', value: 'let parameters: any[]' }, - '**let**', - ], + contents: [{ language: 'typescript', value: 'let parameters: any[]' }, '**let**'], }) }) }) describe('Live updates', function(this: TestContext & ISuiteCallbackContext): void { - - beforeEach(initializeTypeScriptService(createService, rootUri, new Map([ - [rootUri + 'a.ts', 'let parameters = [];'], - ]))) + beforeEach( + initializeTypeScriptService(createService, rootUri, new Map([[rootUri + 'a.ts', 'let parameters = [];']])) + ) afterEach(shutdownService) - it('should handle didChange when configuration is not yet initialized', async function(this: TestContext & ITestCallbackContext): Promise { - + it('should handle didChange when configuration is not yet initialized', async function( + this: TestContext & ITestCallbackContext + ): Promise { const hoverParams = { textDocument: { uri: rootUri + 'a.ts', @@ -1521,25 +1726,26 @@ export function describeTypeScriptService(createService: TypeScriptServiceFactor uri: rootUri + 'a.ts', version: 1, }, - contentChanges: [{ - text: 'let parameters: number[]', - }], + contentChanges: [ + { + text: 'let parameters: number[]', + }, + ], }) - const result: Hover = await this.service.textDocumentHover(hoverParams) + const result: Hover = await this.service + .textDocumentHover(hoverParams) .reduce(applyReducer, null as any) .toPromise() assert.deepEqual(result, { range, - contents: [ - { language: 'typescript', value: 'let parameters: number[]' }, - '**let**', - ], + contents: [{ language: 'typescript', value: 'let parameters: number[]' }, '**let**'], }) }) - it('should handle didClose when configuration is not yet initialized', async function(this: TestContext & ITestCallbackContext): Promise { - + it('should handle didClose when configuration is not yet initialized', async function( + this: TestContext & ITestCallbackContext + ): Promise { const hoverParams = { textDocument: { uri: rootUri + 'a.ts', @@ -1567,20 +1773,17 @@ export function describeTypeScriptService(createService: TypeScriptServiceFactor }, }) - const result: Hover = await this.service.textDocumentHover(hoverParams) + const result: Hover = await this.service + .textDocumentHover(hoverParams) .reduce(applyReducer, null as any) .toPromise() assert.deepEqual(result, { range, - contents: [ - { language: 'typescript', value: 'let parameters: any[]' }, - '**let**', - ], + contents: [{ language: 'typescript', value: 'let parameters: any[]' }, '**let**'], }) }) it('should reflect updated content', async function(this: TestContext & ITestCallbackContext): Promise { - const hoverParams = { textDocument: { uri: rootUri + 'a.ts', @@ -1603,15 +1806,13 @@ export function describeTypeScriptService(createService: TypeScriptServiceFactor } { - const result: Hover = await this.service.textDocumentHover(hoverParams) + const result: Hover = await this.service + .textDocumentHover(hoverParams) .reduce(applyReducer, null as any) .toPromise() assert.deepEqual(result, { range, - contents: [ - { language: 'typescript', value: 'let parameters: any[]' }, - '**let**', - ], + contents: [{ language: 'typescript', value: 'let parameters: any[]' }, '**let**'], }) } @@ -1625,15 +1826,13 @@ export function describeTypeScriptService(createService: TypeScriptServiceFactor }) { - const result: Hover = await this.service.textDocumentHover(hoverParams) + const result: Hover = await this.service + .textDocumentHover(hoverParams) .reduce(applyReducer, null as any) .toPromise() assert.deepEqual(result, { range, - contents: [ - { language: 'typescript', value: 'let parameters: string[]' }, - '**let**', - ], + contents: [{ language: 'typescript', value: 'let parameters: string[]' }, '**let**'], }) } @@ -1642,21 +1841,21 @@ export function describeTypeScriptService(createService: TypeScriptServiceFactor uri: rootUri + 'a.ts', version: 2, }, - contentChanges: [{ - text: 'let parameters: number[]', - }], + contentChanges: [ + { + text: 'let parameters: number[]', + }, + ], }) { - const result: Hover = await this.service.textDocumentHover(hoverParams) + const result: Hover = await this.service + .textDocumentHover(hoverParams) .reduce(applyReducer, null as any) .toPromise() assert.deepEqual(result, { range, - contents: [ - { language: 'typescript', value: 'let parameters: number[]' }, - '**let**', - ], + contents: [{ language: 'typescript', value: 'let parameters: number[]' }, '**let**'], }) } @@ -1667,30 +1866,32 @@ export function describeTypeScriptService(createService: TypeScriptServiceFactor }) { - const result: Hover = await this.service.textDocumentHover(hoverParams) + const result: Hover = await this.service + .textDocumentHover(hoverParams) .reduce(applyReducer, null as any) .toPromise() assert.deepEqual(result, { range, - contents: [ - { language: 'typescript', value: 'let parameters: any[]' }, - '**let**', - ], + contents: [{ language: 'typescript', value: 'let parameters: any[]' }, '**let**'], }) } }) }) describe('Diagnostics', function(this: TestContext & ISuiteCallbackContext): void { - - beforeEach(initializeTypeScriptService(createService, rootUri, new Map([ - [rootUri + 'src/errors.ts', 'const text: string = 33;'], - ]))) + beforeEach( + initializeTypeScriptService( + createService, + rootUri, + new Map([[rootUri + 'src/errors.ts', 'const text: string = 33;']]) + ) + ) afterEach(shutdownService) - it('should publish diagnostics on didOpen', async function(this: TestContext & ITestCallbackContext): Promise { - + it('should publish diagnostics on didOpen', async function( + this: TestContext & ITestCallbackContext + ): Promise { await this.service.textDocumentDidOpen({ textDocument: { uri: rootUri + 'src/errors.ts', @@ -1702,19 +1903,22 @@ export function describeTypeScriptService(createService: TypeScriptServiceFactor sinon.assert.calledOnce(this.client.textDocumentPublishDiagnostics) sinon.assert.calledWithExactly(this.client.textDocumentPublishDiagnostics, { - diagnostics: [{ - message: 'Type \'33\' is not assignable to type \'string\'.', - range: { end: { character: 10, line: 0 }, start: { character: 6, line: 0 } }, - severity: 1, - source: 'ts', - code: 2322, - }], + diagnostics: [ + { + message: "Type '33' is not assignable to type 'string'.", + range: { end: { character: 10, line: 0 }, start: { character: 6, line: 0 } }, + severity: 1, + source: 'ts', + code: 2322, + }, + ], uri: rootUri + 'src/errors.ts', }) }) - it('should publish diagnostics on didChange', async function(this: TestContext & ITestCallbackContext): Promise { - + it('should publish diagnostics on didChange', async function( + this: TestContext & ITestCallbackContext + ): Promise { await this.service.textDocumentDidOpen({ textDocument: { uri: rootUri + 'src/errors.ts', @@ -1731,26 +1935,27 @@ export function describeTypeScriptService(createService: TypeScriptServiceFactor uri: rootUri + 'src/errors.ts', version: 2, }, - contentChanges: [ - { text: 'const text: boolean = 33;' }, - ], + contentChanges: [{ text: 'const text: boolean = 33;' }], }) sinon.assert.calledOnce(this.client.textDocumentPublishDiagnostics) sinon.assert.calledWithExactly(this.client.textDocumentPublishDiagnostics, { - diagnostics: [{ - message: 'Type \'33\' is not assignable to type \'boolean\'.', - range: { end: { character: 10, line: 0 }, start: { character: 6, line: 0 } }, - severity: 1, - source: 'ts', - code: 2322, - }], + diagnostics: [ + { + message: "Type '33' is not assignable to type 'boolean'.", + range: { end: { character: 10, line: 0 }, start: { character: 6, line: 0 } }, + severity: 1, + source: 'ts', + code: 2322, + }, + ], uri: rootUri + 'src/errors.ts', }) }) - it('should publish empty diagnostics on didChange if error was fixed', async function(this: TestContext & ITestCallbackContext): Promise { - + it('should publish empty diagnostics on didChange if error was fixed', async function( + this: TestContext & ITestCallbackContext + ): Promise { await this.service.textDocumentDidOpen({ textDocument: { uri: rootUri + 'src/errors.ts', @@ -1767,9 +1972,7 @@ export function describeTypeScriptService(createService: TypeScriptServiceFactor uri: rootUri + 'src/errors.ts', version: 2, }, - contentChanges: [ - { text: 'const text: number = 33;' }, - ], + contentChanges: [{ text: 'const text: number = 33;' }], }) sinon.assert.calledOnce(this.client.textDocumentPublishDiagnostics) @@ -1779,8 +1982,9 @@ export function describeTypeScriptService(createService: TypeScriptServiceFactor }) }) - it('should clear diagnostics on didClose', async function(this: TestContext & ITestCallbackContext): Promise { - + it('should clear diagnostics on didClose', async function( + this: TestContext & ITestCallbackContext + ): Promise { await this.service.textDocumentDidClose({ textDocument: { uri: rootUri + 'src/errors.ts', @@ -1793,384 +1997,483 @@ export function describeTypeScriptService(createService: TypeScriptServiceFactor uri: rootUri + 'src/errors.ts', }) }) - }) describe('References and imports', function(this: TestContext & ISuiteCallbackContext): void { - beforeEach(initializeTypeScriptService(createService, rootUri, new Map([ - [rootUri + 'a.ts', '/// \nnamespace qux {let f : foo;}'], - [rootUri + 'b.ts', '/// '], - [rootUri + 'c.ts', 'import * as d from "./foo/d"\nd.bar()'], - [rootUri + 'foo/c.ts', 'namespace qux {export interface foo {}}'], - [rootUri + 'foo/d.ts', 'export function bar() {}'], - [rootUri + 'deeprefs/a.ts', '/// \nnamespace qux {\nlet f : foo;\n}'], - [rootUri + 'deeprefs/b.ts', '/// '], - [rootUri + 'deeprefs/c.ts', '/// '], - [rootUri + 'deeprefs/d.ts', '/// '], - [rootUri + 'deeprefs/e.ts', 'namespace qux {\nexport interface foo {}\n}'], - [rootUri + 'missing/a.ts', [ - '/// ', - '/// ', - 'namespace t {', - ' function foo() : Bar {', - ' return null;', - ' }', - '}', - ].join('\n')], - [rootUri + 'missing/b.ts', 'namespace t {\n export interface Bar {\n id?: number;\n }}'], - ]))) + beforeEach( + initializeTypeScriptService( + createService, + rootUri, + new Map([ + [rootUri + 'a.ts', '/// \nnamespace qux {let f : foo;}'], + [rootUri + 'b.ts', '/// '], + [rootUri + 'c.ts', 'import * as d from "./foo/d"\nd.bar()'], + [rootUri + 'foo/c.ts', 'namespace qux {export interface foo {}}'], + [rootUri + 'foo/d.ts', 'export function bar() {}'], + [rootUri + 'deeprefs/a.ts', '/// \nnamespace qux {\nlet f : foo;\n}'], + [rootUri + 'deeprefs/b.ts', '/// '], + [rootUri + 'deeprefs/c.ts', '/// '], + [rootUri + 'deeprefs/d.ts', '/// '], + [rootUri + 'deeprefs/e.ts', 'namespace qux {\nexport interface foo {}\n}'], + [ + rootUri + 'missing/a.ts', + [ + '/// ', + '/// ', + 'namespace t {', + ' function foo() : Bar {', + ' return null;', + ' }', + '}', + ].join('\n'), + ], + [ + rootUri + 'missing/b.ts', + 'namespace t {\n export interface Bar {\n id?: number;\n }}', + ], + ]) + ) + ) afterEach(shutdownService) describe('textDocumentDefinition()', function(this: TestContext & ISuiteCallbackContext): void { - it('should resolve symbol imported with tripe-slash reference', async function(this: TestContext & ITestCallbackContext): Promise { - const result: Location[] = await this.service.textDocumentDefinition({ - textDocument: { - uri: rootUri + 'a.ts', - }, - position: { - line: 1, - character: 23, - }, - }).reduce(applyReducer, null as any).toPromise() - assert.deepEqual(result, [{ - // Note: technically this list should also - // include the 2nd definition of `foo` in - // deeprefs/e.ts, but there's no easy way to - // discover it through file-level imports and - // it is rare enough that we accept this - // omission. (It would probably show up in the - // definition response if the user has already - // navigated to deeprefs/e.ts.) - uri: rootUri + 'foo/c.ts', - range: { - start: { - line: 0, - character: 32, + it('should resolve symbol imported with tripe-slash reference', async function( + this: TestContext & ITestCallbackContext + ): Promise { + const result: Location[] = await this.service + .textDocumentDefinition({ + textDocument: { + uri: rootUri + 'a.ts', }, - end: { - line: 0, - character: 35, + position: { + line: 1, + character: 23, + }, + }) + .reduce(applyReducer, null as any) + .toPromise() + assert.deepEqual(result, [ + { + // Note: technically this list should also + // include the 2nd definition of `foo` in + // deeprefs/e.ts, but there's no easy way to + // discover it through file-level imports and + // it is rare enough that we accept this + // omission. (It would probably show up in the + // definition response if the user has already + // navigated to deeprefs/e.ts.) + uri: rootUri + 'foo/c.ts', + range: { + start: { + line: 0, + character: 32, + }, + end: { + line: 0, + character: 35, + }, }, }, - }]) + ]) }) - it('should resolve symbol imported with import statement', async function(this: TestContext & ITestCallbackContext): Promise { - const result: Location[] = await this.service.textDocumentDefinition({ - textDocument: { - uri: rootUri + 'c.ts', - }, - position: { - line: 1, - character: 2, - }, - }).reduce(applyReducer, null as any).toPromise() - assert.deepEqual(result, [{ - uri: rootUri + 'foo/d.ts', - range: { - start: { - line: 0, - character: 16, + it('should resolve symbol imported with import statement', async function( + this: TestContext & ITestCallbackContext + ): Promise { + const result: Location[] = await this.service + .textDocumentDefinition({ + textDocument: { + uri: rootUri + 'c.ts', }, - end: { - line: 0, - character: 19, + position: { + line: 1, + character: 2, + }, + }) + .reduce(applyReducer, null as any) + .toPromise() + assert.deepEqual(result, [ + { + uri: rootUri + 'foo/d.ts', + range: { + start: { + line: 0, + character: 16, + }, + end: { + line: 0, + character: 19, + }, }, }, - }]) + ]) }) - it('should resolve definition with missing reference', async function(this: TestContext & ITestCallbackContext): Promise { - const result: Location[] = await this.service.textDocumentDefinition({ - textDocument: { - uri: rootUri + 'missing/a.ts', - }, - position: { - line: 3, - character: 21, - }, - }).reduce(applyReducer, null as any).toPromise() - assert.deepEqual(result, [{ - uri: rootUri + 'missing/b.ts', - range: { - start: { - line: 1, + it('should resolve definition with missing reference', async function( + this: TestContext & ITestCallbackContext + ): Promise { + const result: Location[] = await this.service + .textDocumentDefinition({ + textDocument: { + uri: rootUri + 'missing/a.ts', + }, + position: { + line: 3, character: 21, }, - end: { - line: 1, - character: 24, + }) + .reduce(applyReducer, null as any) + .toPromise() + assert.deepEqual(result, [ + { + uri: rootUri + 'missing/b.ts', + range: { + start: { + line: 1, + character: 21, + }, + end: { + line: 1, + character: 24, + }, }, }, - }]) + ]) }) - it('should resolve deep definitions', async function(this: TestContext & ITestCallbackContext): Promise { + it('should resolve deep definitions', async function( + this: TestContext & ITestCallbackContext + ): Promise { // This test passes only because we expect no response from LSP server // for definition located in file references with depth 3 or more (a -> b -> c -> d (...)) // This test will fail once we'll increase (or remove) depth limit - const result: Location[] = await this.service.textDocumentDefinition({ - textDocument: { - uri: rootUri + 'deeprefs/a.ts', - }, - position: { - line: 2, - character: 8, + const result: Location[] = await this.service + .textDocumentDefinition({ + textDocument: { + uri: rootUri + 'deeprefs/a.ts', + }, + position: { + line: 2, + character: 8, + }, + }) + .reduce(applyReducer, null as any) + .toPromise() + assert.deepEqual(result, [ + { + uri: rootUri + 'deeprefs/e.ts', + range: { + start: { + line: 1, + character: 17, + }, + end: { + line: 1, + character: 20, + }, + }, }, - }).reduce(applyReducer, null as any).toPromise() - assert.deepEqual(result, [{ - uri: rootUri + 'deeprefs/e.ts', - range: { - start: { - line: 1, - character: 17, + ]) + }) + }) + }) + + describe('TypeScript libraries', function(this: TestContext & ISuiteCallbackContext): void { + beforeEach( + initializeTypeScriptService( + createService, + rootUri, + new Map([ + [ + rootUri + 'tsconfig.json', + JSON.stringify({ + compilerOptions: { + lib: ['es2016', 'dom'], + }, + }), + ], + [rootUri + 'a.ts', 'function foo(n: Node): {console.log(n.parentNode, NaN})}'], + ]) + ) + ) + + afterEach(shutdownService) + + describe('textDocumentHover()', function(this: TestContext & ISuiteCallbackContext): void { + it('should load local library file', async function( + this: TestContext & ITestCallbackContext + ): Promise { + const result: Hover = await this.service + .textDocumentHover({ + textDocument: { + uri: rootUri + 'a.ts', + }, + position: { + line: 0, + character: 16, }, + }) + .reduce(applyReducer, null as any) + .toPromise() + assert.deepEqual(result, { + range: { end: { - line: 1, character: 20, + line: 0, + }, + start: { + character: 16, + line: 0, }, }, - }]) + contents: [ + { + language: 'typescript', + value: [ + 'interface Node', + 'var Node: {', + ' new (): Node;', + ' prototype: Node;', + ' readonly ATTRIBUTE_NODE: number;', + ' readonly CDATA_SECTION_NODE: number;', + ' readonly COMMENT_NODE: number;', + ' readonly DOCUMENT_FRAGMENT_NODE: number;', + ' readonly DOCUMENT_NODE: number;', + ' readonly DOCUMENT_POSITION_CONTAINED_BY: number;', + ' readonly DOCUMENT_POSITION_CONTAINS: number;', + ' readonly DOCUMENT_POSITION_DISCONNECTED: number;', + ' readonly DOCUMENT_POSITION_FOLLOWING: number;', + ' readonly DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC: number;', + ' readonly DOCUMENT_POSITION_PRECEDING: number;', + ' readonly DOCUMENT_TYPE_NODE: number;', + ' readonly ELEMENT_NODE: number;', + ' readonly ENTITY_NODE: number;', + ' readonly ENTITY_REFERENCE_NODE: number;', + ' readonly NOTATION_NODE: number;', + ' readonly PROCESSING_INSTRUCTION_NODE: number;', + ' readonly TEXT_NODE: number;', + '}', + ].join('\n'), + }, + '**var** _(ambient)_', + ], + }) + }) + }) + describe('textDocumentDefinition()', function(this: TestContext & ISuiteCallbackContext): void { + it('should resolve TS libraries to github URL', async function( + this: TestContext & ITestCallbackContext + ): Promise { + assert.deepEqual( + await this.service + .textDocumentDefinition({ + textDocument: { + uri: rootUri + 'a.ts', + }, + position: { + line: 0, + character: 16, + }, + }) + .reduce(applyReducer, null as any) + .toPromise(), + [ + { + uri: 'git://github.com/Microsoft/TypeScript?v' + ts.version + '#lib/lib.dom.d.ts', + range: { + start: { + line: 8259, + character: 10, + }, + end: { + line: 8259, + character: 14, + }, + }, + }, + { + uri: 'git://github.com/Microsoft/TypeScript?v' + ts.version + '#lib/lib.dom.d.ts', + range: { + start: { + line: 8311, + character: 12, + }, + end: { + line: 8311, + character: 16, + }, + }, + }, + ] + ) + + assert.deepEqual( + await this.service + .textDocumentDefinition({ + textDocument: { + uri: rootUri + 'a.ts', + }, + position: { + line: 0, + character: 50, + }, + }) + .reduce(applyReducer, null as any) + .toPromise(), + [ + { + uri: 'git://github.com/Microsoft/TypeScript?v' + ts.version + '#lib/lib.es5.d.ts', + range: { + start: { + line: 24, + character: 14, + }, + end: { + line: 24, + character: 17, + }, + }, + }, + ] + ) }) }) }) - describe('TypeScript libraries', function(this: TestContext & ISuiteCallbackContext): void { - beforeEach(initializeTypeScriptService(createService, rootUri, new Map([ - [rootUri + 'tsconfig.json', JSON.stringify({ - compilerOptions: { - lib: ['es2016', 'dom'], - }, - })], - [rootUri + 'a.ts', 'function foo(n: Node): {console.log(n.parentNode, NaN})}'], - ]))) + describe('textDocumentReferences()', function(this: TestContext & ISuiteCallbackContext): void { + beforeEach( + initializeTypeScriptService( + createService, + rootUri, + new Map([ + [ + rootUri + 'a.ts', + [ + 'class A {', + ' /** foo doc*/', + ' foo() {}', + ' /** bar doc*/', + ' bar(): number { return 1; }', + ' /** ', + ' * The Baz function', + ' * @param num Number parameter', + ' * @param text Text parameter', + ' */', + ' baz(num: number, text: string): string { return ""; }', + ' /** qux doc*/', + ' qux: number;', + '}', + 'const a = new A();', + 'a.baz(32, sd)', + ].join('\n'), + ], + [rootUri + 'uses-import.ts', ['import * as i from "./import"', 'i.d()'].join('\n')], + [rootUri + 'also-uses-import.ts', ['import {d} from "./import"', 'd()'].join('\n')], + [rootUri + 'import.ts', '/** d doc*/ export function d() {}'], + ]) + ) + ) afterEach(shutdownService) - describe('textDocumentHover()', function(this: TestContext & ISuiteCallbackContext): void { - it('should load local library file', async function(this: TestContext & ITestCallbackContext): Promise { - const result: Hover = await this.service.textDocumentHover({ + it('should provide an empty response when no reference is found', async function( + this: TestContext & ITestCallbackContext + ): Promise { + const result = await this.service + .textDocumentReferences({ textDocument: { uri: rootUri + 'a.ts', }, position: { line: 0, - character: 16, - }, - }).reduce(applyReducer, null as any).toPromise() - assert.deepEqual(result, { - range: { - end: { - character: 20, - line: 0, - }, - start: { - character: 16, - line: 0, - }, + character: 0, }, - contents: [ - { - language: 'typescript', - value: [ - 'interface Node', - 'var Node: {', - ' new (): Node;', - ' prototype: Node;', - ' readonly ATTRIBUTE_NODE: number;', - ' readonly CDATA_SECTION_NODE: number;', - ' readonly COMMENT_NODE: number;', - ' readonly DOCUMENT_FRAGMENT_NODE: number;', - ' readonly DOCUMENT_NODE: number;', - ' readonly DOCUMENT_POSITION_CONTAINED_BY: number;', - ' readonly DOCUMENT_POSITION_CONTAINS: number;', - ' readonly DOCUMENT_POSITION_DISCONNECTED: number;', - ' readonly DOCUMENT_POSITION_FOLLOWING: number;', - ' readonly DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC: number;', - ' readonly DOCUMENT_POSITION_PRECEDING: number;', - ' readonly DOCUMENT_TYPE_NODE: number;', - ' readonly ELEMENT_NODE: number;', - ' readonly ENTITY_NODE: number;', - ' readonly ENTITY_REFERENCE_NODE: number;', - ' readonly NOTATION_NODE: number;', - ' readonly PROCESSING_INSTRUCTION_NODE: number;', - ' readonly TEXT_NODE: number;', - '}', - ].join('\n'), - }, - '**var** _(ambient)_', - ], + context: { includeDeclaration: false }, }) - }) - }) - describe('textDocumentDefinition()', function(this: TestContext & ISuiteCallbackContext): void { - it('should resolve TS libraries to github URL', async function(this: TestContext & ITestCallbackContext): Promise { - assert.deepEqual(await this.service.textDocumentDefinition({ + .reduce(applyReducer, null as any) + .toPromise() + assert.deepEqual(result, []) + }) + + it('should include the declaration if requested', async function( + this: TestContext & ITestCallbackContext + ): Promise { + const result = await this.service + .textDocumentReferences({ textDocument: { uri: rootUri + 'a.ts', }, position: { - line: 0, - character: 16, + line: 4, + character: 5, }, - }).reduce(applyReducer, null as any).toPromise(), [{ - uri: 'git://github.com/Microsoft/TypeScript?v' + ts.version + '#lib/lib.dom.d.ts', + context: { includeDeclaration: true }, + }) + .reduce(applyReducer, null as any) + .toPromise() + assert.deepEqual(result, [ + { range: { - start: { - line: 8259, - character: 10, - }, end: { - line: 8259, - character: 14, + character: 7, + line: 4, }, - }, - }, { - uri: 'git://github.com/Microsoft/TypeScript?v' + ts.version + '#lib/lib.dom.d.ts', - range: { start: { - line: 8311, - character: 12, - }, - end: { - line: 8311, - character: 16, + character: 4, + line: 4, }, }, - }]) + uri: rootUri + 'a.ts', + }, + ]) + }) - assert.deepEqual(await this.service.textDocumentDefinition({ + it('should provide a reference within the same file', async function( + this: TestContext & ITestCallbackContext + ): Promise { + const result = await this.service + .textDocumentReferences({ textDocument: { uri: rootUri + 'a.ts', }, position: { - line: 0, - character: 50, + line: 10, + character: 5, }, - }).reduce(applyReducer, null as any).toPromise(), [{ - uri: 'git://github.com/Microsoft/TypeScript?v' + ts.version + '#lib/lib.es5.d.ts', + context: { includeDeclaration: false }, + }) + .reduce(applyReducer, null as any) + .toPromise() + assert.deepEqual(result, [ + { range: { - start: { - line: 24, - character: 14, - }, end: { - line: 24, - character: 17, + character: 5, + line: 15, + }, + start: { + character: 2, + line: 15, }, }, - }]) - }) - }) - }) - - describe('textDocumentReferences()', function(this: TestContext & ISuiteCallbackContext): void { - beforeEach(initializeTypeScriptService(createService, rootUri, new Map([ - [rootUri + 'a.ts', [ - 'class A {', - ' /** foo doc*/', - ' foo() {}', - ' /** bar doc*/', - ' bar(): number { return 1; }', - ' /** ', - ' * The Baz function', - ' * @param num Number parameter', - ' * @param text Text parameter', - ' */', - ' baz(num: number, text: string): string { return ""; }', - ' /** qux doc*/', - ' qux: number;', - '}', - 'const a = new A();', - 'a.baz(32, sd)', - ].join('\n')], - [rootUri + 'uses-import.ts', [ - 'import * as i from "./import"', - 'i.d()', - ].join('\n')], - [rootUri + 'also-uses-import.ts', [ - 'import {d} from "./import"', - 'd()', - ].join('\n')], - [rootUri + 'import.ts', '/** d doc*/ export function d() {}'], - ]))) - - afterEach(shutdownService) - - it('should provide an empty response when no reference is found', async function(this: TestContext & ITestCallbackContext): Promise { - const result = await this.service.textDocumentReferences({ - textDocument: { - uri: rootUri + 'a.ts', - }, - position: { - line: 0, - character: 0, - }, - context: { includeDeclaration: false }, - }).reduce(applyReducer, null as any).toPromise() - assert.deepEqual(result, []) - }) - - it('should include the declaration if requested', async function(this: TestContext & ITestCallbackContext): Promise { - const result = await this.service.textDocumentReferences({ - textDocument: { uri: rootUri + 'a.ts', }, - position: { - line: 4, - character: 5, - }, - context: { includeDeclaration: true }, - }).reduce(applyReducer, null as any).toPromise() - assert.deepEqual(result, [{ - range: { - end: { - character: 7, - line: 4, - }, - start: { - character: 4, - line: 4, - }, - }, - uri: rootUri + 'a.ts', - }]) + ]) }) - - it('should provide a reference within the same file', async function(this: TestContext & ITestCallbackContext): Promise { - const result = await this.service.textDocumentReferences({ - textDocument: { - uri: rootUri + 'a.ts', - }, - position: { - line: 10, - character: 5, - }, - context: { includeDeclaration: false }, - }).reduce(applyReducer, null as any).toPromise() - assert.deepEqual(result, [{ - range: { - end: { - character: 5, - line: 15, + it('should provide two references from imports', async function( + this: TestContext & ITestCallbackContext + ): Promise { + const result = await this.service + .textDocumentReferences({ + textDocument: { + uri: rootUri + 'import.ts', }, - start: { - character: 2, - line: 15, + position: { + line: 0, + character: 28, }, - }, - uri: rootUri + 'a.ts', - }]) - }) - it('should provide two references from imports', async function(this: TestContext & ITestCallbackContext): Promise { - const result = await this.service.textDocumentReferences({ - textDocument: { - uri: rootUri + 'import.ts', - }, - position: { - line: 0, - character: 28, - }, - context: { includeDeclaration: false }, - }).reduce(applyReducer, null as any).toPromise() + context: { includeDeclaration: false }, + }) + .reduce(applyReducer, null as any) + .toPromise() assert.deepEqual(result, [ { range: { @@ -2203,55 +2506,64 @@ export function describeTypeScriptService(createService: TypeScriptServiceFactor }) describe('textDocumentSignatureHelp()', function(this: TestContext & ISuiteCallbackContext): void { - beforeEach(initializeTypeScriptService(createService, rootUri, new Map([ - [rootUri + 'a.ts', [ - 'class A {', - ' /** foo doc*/', - ' foo() {}', - ' /** bar doc*/', - ' bar(): number { return 1; }', - ' /** ', - ' * The Baz function', - ' * @param num Number parameter', - ' * @param text Text parameter', - ' */', - ' baz(num: number, text: string): string { return ""; }', - ' /** qux doc*/', - ' qux: number;', - '}', - 'const a = new A();', - 'a.baz(32, sd)', - ].join('\n')], - [rootUri + 'uses-import.ts', [ - 'import * as i from "./import"', - 'i.d()', - ].join('\n')], - [rootUri + 'import.ts', '/** d doc*/ export function d() {}'], - [rootUri + 'uses-reference.ts', [ - '/// ', - 'let z : foo.', - ].join('\n')], - [rootUri + 'reference.ts', [ - 'namespace foo {', - ' /** bar doc*/', - ' export interface bar {}', - '}', - ].join('\n')], - [rootUri + 'empty.ts', ''], - ]))) + beforeEach( + initializeTypeScriptService( + createService, + rootUri, + new Map([ + [ + rootUri + 'a.ts', + [ + 'class A {', + ' /** foo doc*/', + ' foo() {}', + ' /** bar doc*/', + ' bar(): number { return 1; }', + ' /** ', + ' * The Baz function', + ' * @param num Number parameter', + ' * @param text Text parameter', + ' */', + ' baz(num: number, text: string): string { return ""; }', + ' /** qux doc*/', + ' qux: number;', + '}', + 'const a = new A();', + 'a.baz(32, sd)', + ].join('\n'), + ], + [rootUri + 'uses-import.ts', ['import * as i from "./import"', 'i.d()'].join('\n')], + [rootUri + 'import.ts', '/** d doc*/ export function d() {}'], + [ + rootUri + 'uses-reference.ts', + ['/// ', 'let z : foo.'].join('\n'), + ], + [ + rootUri + 'reference.ts', + ['namespace foo {', ' /** bar doc*/', ' export interface bar {}', '}'].join('\n'), + ], + [rootUri + 'empty.ts', ''], + ]) + ) + ) afterEach(shutdownService) - it('should provide a valid empty response when no signature is found', async function(this: TestContext & ITestCallbackContext): Promise { - const result: SignatureHelp = await this.service.textDocumentSignatureHelp({ - textDocument: { - uri: rootUri + 'a.ts', - }, - position: { - line: 0, - character: 0, - }, - }).reduce(applyReducer, null as any).toPromise() + it('should provide a valid empty response when no signature is found', async function( + this: TestContext & ITestCallbackContext + ): Promise { + const result: SignatureHelp = await this.service + .textDocumentSignatureHelp({ + textDocument: { + uri: rootUri + 'a.ts', + }, + position: { + line: 0, + character: 0, + }, + }) + .reduce(applyReducer, null as any) + .toPromise() assert.deepEqual(result, { signatures: [], activeSignature: 0, @@ -2259,28 +2571,36 @@ export function describeTypeScriptService(createService: TypeScriptServiceFactor }) }) - it('should provide signature help with parameters in the same file', async function(this: TestContext & ITestCallbackContext): Promise { - const result: SignatureHelp = await this.service.textDocumentSignatureHelp({ - textDocument: { - uri: rootUri + 'a.ts', - }, - position: { - line: 15, - character: 11, - }, - }).reduce(applyReducer, null as any).toPromise() + it('should provide signature help with parameters in the same file', async function( + this: TestContext & ITestCallbackContext + ): Promise { + const result: SignatureHelp = await this.service + .textDocumentSignatureHelp({ + textDocument: { + uri: rootUri + 'a.ts', + }, + position: { + line: 15, + character: 11, + }, + }) + .reduce(applyReducer, null as any) + .toPromise() assert.deepEqual(result, { signatures: [ { label: 'baz(num: number, text: string): string', documentation: 'The Baz function', - parameters: [{ - label: 'num: number', - documentation: 'Number parameter', - }, { - label: 'text: string', - documentation: 'Text parameter', - }], + parameters: [ + { + label: 'num: number', + documentation: 'Number parameter', + }, + { + label: 'text: string', + documentation: 'Text parameter', + }, + ], }, ], activeSignature: 0, @@ -2288,68 +2608,87 @@ export function describeTypeScriptService(createService: TypeScriptServiceFactor }) }) - it('should provide signature help from imported symbols', async function(this: TestContext & ITestCallbackContext): Promise { - const result: SignatureHelp = await this.service.textDocumentSignatureHelp({ - textDocument: { - uri: rootUri + 'uses-import.ts', - }, - position: { - line: 1, - character: 4, - }, - }).reduce(applyReducer, null as any).toPromise() + it('should provide signature help from imported symbols', async function( + this: TestContext & ITestCallbackContext + ): Promise { + const result: SignatureHelp = await this.service + .textDocumentSignatureHelp({ + textDocument: { + uri: rootUri + 'uses-import.ts', + }, + position: { + line: 1, + character: 4, + }, + }) + .reduce(applyReducer, null as any) + .toPromise() assert.deepEqual(result, { activeSignature: 0, activeParameter: 0, - signatures: [{ - label: 'd(): void', - documentation: 'd doc', - parameters: [], - }], + signatures: [ + { + label: 'd(): void', + documentation: 'd doc', + parameters: [], + }, + ], }) }) - }) describe('textDocumentCompletion() with snippets', function(this: TestContext & ISuiteCallbackContext): void { - beforeEach(initializeTypeScriptService(createService, rootUri, new Map([ - [rootUri + 'a.ts', [ - 'class A {', - ' /** foo doc*/', - ' foo() {}', - ' /** bar doc*/', - ' bar(num: number): number { return 1; }', - ' /** baz doc*/', - ' baz(num: number): string { return ""; }', - ' /** qux doc*/', - ' qux: number;', - '}', - 'const a = new A();', - 'a.', - ].join('\n')], - ]), { - textDocument: { - completion: { - completionItem: { - snippetSupport: true, + beforeEach( + initializeTypeScriptService( + createService, + rootUri, + new Map([ + [ + rootUri + 'a.ts', + [ + 'class A {', + ' /** foo doc*/', + ' foo() {}', + ' /** bar doc*/', + ' bar(num: number): number { return 1; }', + ' /** baz doc*/', + ' baz(num: number): string { return ""; }', + ' /** qux doc*/', + ' qux: number;', + '}', + 'const a = new A();', + 'a.', + ].join('\n'), + ], + ]), + { + textDocument: { + completion: { + completionItem: { + snippetSupport: true, + }, + }, }, - }, - }, - ...DEFAULT_CAPABILITIES, - })) + ...DEFAULT_CAPABILITIES, + } + ) + ) afterEach(shutdownService) it('should produce completions', async function(this: TestContext & ITestCallbackContext): Promise { - const result: CompletionList = await this.service.textDocumentCompletion({ - textDocument: { - uri: rootUri + 'a.ts', - }, - position: { - line: 11, - character: 2, - }, - }).reduce(applyReducer, null as any).toPromise() + const result: CompletionList = await this.service + .textDocumentCompletion({ + textDocument: { + uri: rootUri + 'a.ts', + }, + position: { + line: 11, + character: 2, + }, + }) + .reduce(applyReducer, null as any) + .toPromise() assert.equal(result.isIncomplete, false) assert.sameDeepMembers(result.items, [ { @@ -2395,16 +2734,21 @@ export function describeTypeScriptService(createService: TypeScriptServiceFactor ]) }) - it('should resolve completions with snippets', async function(this: TestContext & ITestCallbackContext): Promise { - const result: CompletionList = await this.service.textDocumentCompletion({ - textDocument: { - uri: rootUri + 'a.ts', - }, - position: { - line: 11, - character: 2, - }, - }).reduce(applyReducer, null as any).toPromise() + it('should resolve completions with snippets', async function( + this: TestContext & ITestCallbackContext + ): Promise { + const result: CompletionList = await this.service + .textDocumentCompletion({ + textDocument: { + uri: rootUri + 'a.ts', + }, + position: { + line: 11, + character: 2, + }, + }) + .reduce(applyReducer, null as any) + .toPromise() // * A snippet can define tab stops and placeholders with `$1`, `$2` // * and `${3:foo}`. `$0` defines the final tab stop, it defaults to // * the end of the snippet. Placeholders with equal identifiers are linked, @@ -2412,9 +2756,10 @@ export function describeTypeScriptService(createService: TypeScriptServiceFactor assert.equal(result.isIncomplete, false) const resolvedItems = await Observable.from(result.items) - .mergeMap(item => this.service - .completionItemResolve(item) - .reduce(applyReducer, null as any) + .mergeMap(item => + this.service + .completionItemResolve(item) + .reduce(applyReducer, null as any) ) .toArray() .toPromise() @@ -2467,51 +2812,60 @@ export function describeTypeScriptService(createService: TypeScriptServiceFactor }) describe('textDocumentCompletion()', function(this: TestContext & ISuiteCallbackContext): void { - beforeEach(initializeTypeScriptService(createService, rootUri, new Map([ - [rootUri + 'a.ts', [ - 'class A {', - ' /** foo doc*/', - ' foo() {}', - ' /** bar doc*/', - ' bar(): number { return 1; }', - ' /** baz doc*/', - ' baz(): string { return ""; }', - ' /** qux doc*/', - ' qux: number;', - '}', - 'const a = new A();', - 'a.', - ].join('\n')], - [rootUri + 'uses-import.ts', [ - 'import * as i from "./import"', - 'i.', - ].join('\n')], - [rootUri + 'import.ts', '/** d doc*/ export function d() {}'], - [rootUri + 'uses-reference.ts', [ - '/// ', - 'let z : foo.', - ].join('\n')], - [rootUri + 'reference.ts', [ - 'namespace foo {', - ' /** bar doc*/', - ' export interface bar {}', - '}', - ].join('\n')], - [rootUri + 'empty.ts', ''], - ]))) + beforeEach( + initializeTypeScriptService( + createService, + rootUri, + new Map([ + [ + rootUri + 'a.ts', + [ + 'class A {', + ' /** foo doc*/', + ' foo() {}', + ' /** bar doc*/', + ' bar(): number { return 1; }', + ' /** baz doc*/', + ' baz(): string { return ""; }', + ' /** qux doc*/', + ' qux: number;', + '}', + 'const a = new A();', + 'a.', + ].join('\n'), + ], + [rootUri + 'uses-import.ts', ['import * as i from "./import"', 'i.'].join('\n')], + [rootUri + 'import.ts', '/** d doc*/ export function d() {}'], + [ + rootUri + 'uses-reference.ts', + ['/// ', 'let z : foo.'].join('\n'), + ], + [ + rootUri + 'reference.ts', + ['namespace foo {', ' /** bar doc*/', ' export interface bar {}', '}'].join('\n'), + ], + [rootUri + 'empty.ts', ''], + ]) + ) + ) afterEach(shutdownService) - it('produces completions in the same file', async function(this: TestContext & ITestCallbackContext): Promise { - const result: CompletionList = await this.service.textDocumentCompletion({ - textDocument: { - uri: rootUri + 'a.ts', - }, - position: { - line: 11, - character: 2, - }, - }).reduce(applyReducer, null as any).toPromise() + it('produces completions in the same file', async function( + this: TestContext & ITestCallbackContext + ): Promise { + const result: CompletionList = await this.service + .textDocumentCompletion({ + textDocument: { + uri: rootUri + 'a.ts', + }, + position: { + line: 11, + character: 2, + }, + }) + .reduce(applyReducer, null as any) + .toPromise() assert.equal(result.isIncomplete, false) assert.sameDeepMembers(result.items, [ { @@ -2557,21 +2911,28 @@ export function describeTypeScriptService(createService: TypeScriptServiceFactor ]) }) - it('resolves completions in the same file', async function(this: TestContext & ITestCallbackContext): Promise { - const result: CompletionList = await this.service.textDocumentCompletion({ - textDocument: { - uri: rootUri + 'a.ts', - }, - position: { - line: 11, - character: 2, - }, - }).reduce(applyReducer, null as any).toPromise() + it('resolves completions in the same file', async function( + this: TestContext & ITestCallbackContext + ): Promise { + const result: CompletionList = await this.service + .textDocumentCompletion({ + textDocument: { + uri: rootUri + 'a.ts', + }, + position: { + line: 11, + character: 2, + }, + }) + .reduce(applyReducer, null as any) + .toPromise() assert.equal(result.isIncomplete, false) - const resolveItem = (item: CompletionItem) => this.service + const resolveItem = (item: CompletionItem) => + this.service .completionItemResolve(item) - .reduce(applyReducer, null as any).toPromise() + .reduce(applyReducer, null as any) + .toPromise() const resolvedItems = await Promise.all(result.items.map(resolveItem)) @@ -2617,222 +2978,280 @@ export function describeTypeScriptService(createService: TypeScriptServiceFactor data: undefined, }, ]) - }) - it('produces completions for imported symbols', async function(this: TestContext & ITestCallbackContext): Promise { - const result: CompletionList = await this.service.textDocumentCompletion({ - textDocument: { - uri: rootUri + 'uses-import.ts', - }, - position: { - line: 1, - character: 2, - }, - }).reduce(applyReducer, null as any).toPromise() + it('produces completions for imported symbols', async function( + this: TestContext & ITestCallbackContext + ): Promise { + const result: CompletionList = await this.service + .textDocumentCompletion({ + textDocument: { + uri: rootUri + 'uses-import.ts', + }, + position: { + line: 1, + character: 2, + }, + }) + .reduce(applyReducer, null as any) + .toPromise() assert.deepEqual(result, { isIncomplete: false, - items: [{ - data: { - entryName: 'd', - offset: 32, - uri: rootUri + 'uses-import.ts', + items: [ + { + data: { + entryName: 'd', + offset: 32, + uri: rootUri + 'uses-import.ts', + }, + label: 'd', + kind: CompletionItemKind.Function, + sortText: '0', }, - label: 'd', - kind: CompletionItemKind.Function, - sortText: '0', - }], + ], }) }) - it('produces completions for referenced symbols', async function(this: TestContext & ITestCallbackContext): Promise { - const result: CompletionList = await this.service.textDocumentCompletion({ - textDocument: { - uri: rootUri + 'uses-reference.ts', - }, - position: { - line: 1, - character: 13, - }, - }).reduce(applyReducer, null as any).toPromise() + it('produces completions for referenced symbols', async function( + this: TestContext & ITestCallbackContext + ): Promise { + const result: CompletionList = await this.service + .textDocumentCompletion({ + textDocument: { + uri: rootUri + 'uses-reference.ts', + }, + position: { + line: 1, + character: 13, + }, + }) + .reduce(applyReducer, null as any) + .toPromise() assert.deepEqual(result, { isIncomplete: false, - items: [{ - data: { - entryName: 'bar', - offset: 51, - uri: rootUri + 'uses-reference.ts', + items: [ + { + data: { + entryName: 'bar', + offset: 51, + uri: rootUri + 'uses-reference.ts', + }, + label: 'bar', + kind: CompletionItemKind.Interface, + sortText: '0', }, - label: 'bar', - kind: CompletionItemKind.Interface, - sortText: '0', - }], + ], }) }) - it('produces completions for empty files', async function(this: TestContext & ITestCallbackContext): Promise { + it('produces completions for empty files', async function( + this: TestContext & ITestCallbackContext + ): Promise { this.timeout(10000) - const result: CompletionList = await this.service.textDocumentCompletion({ - textDocument: { - uri: rootUri + 'empty.ts', - }, - position: { - line: 0, - character: 0, - }, - }).reduce(applyReducer, null as any).toPromise() + const result: CompletionList = await this.service + .textDocumentCompletion({ + textDocument: { + uri: rootUri + 'empty.ts', + }, + position: { + line: 0, + character: 0, + }, + }) + .reduce(applyReducer, null as any) + .toPromise() assert.notDeepEqual(result.items, []) }) }) describe('textDocumentRename()', function(this: TestContext & ISuiteCallbackContext): void { - beforeEach(initializeTypeScriptService(createService, rootUri, new Map([ - [rootUri + 'package.json', JSON.stringify({ name: 'mypkg' })], - [rootUri + 'a.ts', [ - 'class A {', - ' /** foo doc*/', - ' foo() {}', - ' /** bar doc*/', - ' bar(): number { return 1; }', - ' /** baz doc*/', - ' baz(): string { return ""; }', - ' /** qux doc*/', - ' qux: number;', - '}', - 'const a = new A();', - 'a.', - ].join('\n')], - [rootUri + 'uses-import.ts', [ - 'import {d} from "./import"', - 'const x = d();', - ].join('\n')], - [rootUri + 'import.ts', 'export function d(): number { return 55; }'], - ]))) + beforeEach( + initializeTypeScriptService( + createService, + rootUri, + new Map([ + [rootUri + 'package.json', JSON.stringify({ name: 'mypkg' })], + [ + rootUri + 'a.ts', + [ + 'class A {', + ' /** foo doc*/', + ' foo() {}', + ' /** bar doc*/', + ' bar(): number { return 1; }', + ' /** baz doc*/', + ' baz(): string { return ""; }', + ' /** qux doc*/', + ' qux: number;', + '}', + 'const a = new A();', + 'a.', + ].join('\n'), + ], + [rootUri + 'uses-import.ts', ['import {d} from "./import"', 'const x = d();'].join('\n')], + [rootUri + 'import.ts', 'export function d(): number { return 55; }'], + ]) + ) + ) afterEach(shutdownService) - it('should error on an invalid symbol', async function(this: TestContext & ITestCallbackContext): Promise { - await Promise.resolve(assert.isRejected( - this.service.textDocumentRename({ + it('should error on an invalid symbol', async function( + this: TestContext & ITestCallbackContext + ): Promise { + await Promise.resolve( + assert.isRejected( + this.service + .textDocumentRename({ + textDocument: { + uri: rootUri + 'a.ts', + }, + position: { + line: 0, + character: 1, + }, + newName: 'asdf', + }) + .reduce(applyReducer, null as any) + .toPromise(), + 'This symbol cannot be renamed' + ) + ) + }) + it('should return a correct WorkspaceEdit to rename a class', async function( + this: TestContext & ITestCallbackContext + ): Promise { + const result: WorkspaceEdit = await this.service + .textDocumentRename({ textDocument: { uri: rootUri + 'a.ts', }, position: { line: 0, - character: 1, + character: 6, }, - newName: 'asdf', - }).reduce(applyReducer, null as any).toPromise(), - 'This symbol cannot be renamed' - )) - }) - it('should return a correct WorkspaceEdit to rename a class', async function(this: TestContext & ITestCallbackContext): Promise { - const result: WorkspaceEdit = await this.service.textDocumentRename({ - textDocument: { - uri: rootUri + 'a.ts', - }, - position: { - line: 0, - character: 6, - }, - newName: 'B', - }).reduce(applyReducer, null as any).toPromise() + newName: 'B', + }) + .reduce(applyReducer, null as any) + .toPromise() assert.deepEqual(result, { changes: { - [rootUri + 'a.ts']: [{ - newText: 'B', - range: { - end: { - character: 7, - line: 0, - }, - start: { - character: 6, - line: 0, + [rootUri + 'a.ts']: [ + { + newText: 'B', + range: { + end: { + character: 7, + line: 0, + }, + start: { + character: 6, + line: 0, + }, }, }, - }, { - newText: 'B', - range: { - end: { - character: 15, - line: 10, - }, - start: { - character: 14, - line: 10, + { + newText: 'B', + range: { + end: { + character: 15, + line: 10, + }, + start: { + character: 14, + line: 10, + }, }, }, - }], + ], }, }) }) - it('should return a correct WorkspaceEdit to rename an imported function', async function(this: TestContext & ITestCallbackContext): Promise { - const result: WorkspaceEdit = await this.service.textDocumentRename({ - textDocument: { - uri: rootUri + 'import.ts', - }, - position: { - line: 0, - character: 16, - }, - newName: 'f', - }).reduce(applyReducer, null as any).toPromise() + it('should return a correct WorkspaceEdit to rename an imported function', async function( + this: TestContext & ITestCallbackContext + ): Promise { + const result: WorkspaceEdit = await this.service + .textDocumentRename({ + textDocument: { + uri: rootUri + 'import.ts', + }, + position: { + line: 0, + character: 16, + }, + newName: 'f', + }) + .reduce(applyReducer, null as any) + .toPromise() assert.deepEqual(result, { changes: { - [rootUri + 'import.ts']: [{ - newText: 'f', - range: { - end: { - character: 17, - line: 0, - }, - start: { - character: 16, - line: 0, + [rootUri + 'import.ts']: [ + { + newText: 'f', + range: { + end: { + character: 17, + line: 0, + }, + start: { + character: 16, + line: 0, + }, }, }, - }], - [rootUri + 'uses-import.ts']: [{ - newText: 'f', - range: { - end: { - character: 9, - line: 0, - }, - start: { - character: 8, - line: 0, + ], + [rootUri + 'uses-import.ts']: [ + { + newText: 'f', + range: { + end: { + character: 9, + line: 0, + }, + start: { + character: 8, + line: 0, + }, }, }, - }, { - newText: 'f', - range: { - end: { - character: 11, - line: 1, - }, - start: { - character: 10, - line: 1, + { + newText: 'f', + range: { + end: { + character: 11, + line: 1, + }, + start: { + character: 10, + line: 1, + }, }, }, - }], + ], }, }) }) }) describe('textDocumentCodeAction()', function(this: TestContext & ISuiteCallbackContext): void { - beforeEach(initializeTypeScriptService(createService, rootUri, new Map([ - [rootUri + 'package.json', JSON.stringify({ name: 'mypkg' })], - [rootUri + 'a.ts', [ - 'class A {', - '\tconstructor() {', - '\t\tmissingThis = 33;', - '\t}', - '}', - 'const a = new A();', - ].join('\n')], - ]))) + beforeEach( + initializeTypeScriptService( + createService, + rootUri, + new Map([ + [rootUri + 'package.json', JSON.stringify({ name: 'mypkg' })], + [ + rootUri + 'a.ts', + [ + 'class A {', + '\tconstructor() {', + '\t\tmissingThis = 33;', + '\t}', + '}', + 'const a = new A();', + ].join('\n'), + ], + ]) + ) + ) afterEach(shutdownService) @@ -2859,81 +3278,109 @@ export function describeTypeScriptService(createService: TypeScriptServiceFactor start: { line: 3, character: 4 }, end: { line: 3, character: 15 }, }, - message: 'Cannot find name \'missingThis\'. Did you mean the instance member \'this.missingThis\'?', + message: "Cannot find name 'missingThis'. Did you mean the instance member 'this.missingThis'?", severity: DiagnosticSeverity.Error, code: 2663, source: 'ts', } - const actions: Command[] = await this.service.textDocumentCodeAction({ - textDocument: { - uri: rootUri + 'a.ts', - }, - range: firstDiagnostic.range, - context: { - diagnostics: [firstDiagnostic], + const actions: Command[] = await this.service + .textDocumentCodeAction({ + textDocument: { + uri: rootUri + 'a.ts', + }, + range: firstDiagnostic.range, + context: { + diagnostics: [firstDiagnostic], + }, + }) + .reduce(applyReducer, null as any) + .toPromise() + assert.deepEqual(actions, [ + { + title: "Add 'this.' to unresolved variable.", + command: 'codeFix', + arguments: [ + { + fileName: toUnixPath(uri2path(rootUri + 'a.ts')), // path only used by TS service + textChanges: [ + { + span: { start: 49, length: 13 }, + newText: '\t\tthis.missingThis', + }, + ], + }, + ], }, - }).reduce(applyReducer, null as any).toPromise() - assert.deepEqual(actions, [{ - title: 'Add \'this.\' to unresolved variable.', - command: 'codeFix', - arguments: [{ - fileName: toUnixPath(uri2path(rootUri + 'a.ts')), // path only used by TS service - textChanges: [{ - span: { start: 49, length: 13 }, - newText: '\t\tthis.missingThis', - }], - }], - }]) - + ]) }) }) describe('workspaceExecuteCommand()', function(this: TestContext & ISuiteCallbackContext): void { - beforeEach(initializeTypeScriptService(createService, rootUri, new Map([ - [rootUri + 'package.json', JSON.stringify({ name: 'mypkg' })], - [rootUri + 'a.ts', [ - 'class A {', - ' constructor() {', - ' missingThis = 33;', - ' }', - '}', - 'const a = new A();', - ].join('\n')], - ]))) + beforeEach( + initializeTypeScriptService( + createService, + rootUri, + new Map([ + [rootUri + 'package.json', JSON.stringify({ name: 'mypkg' })], + [ + rootUri + 'a.ts', + [ + 'class A {', + ' constructor() {', + ' missingThis = 33;', + ' }', + '}', + 'const a = new A();', + ].join('\n'), + ], + ]) + ) + ) afterEach(shutdownService) describe('codeFix', () => { - it('should apply a WorkspaceEdit for the passed FileTextChanges', async function(this: TestContext & ITestCallbackContext): Promise { - await this.service.workspaceExecuteCommand({ - command: 'codeFix', - arguments: [{ - fileName: uri2path(rootUri + 'a.ts'), - textChanges: [{ - span: { start: 50, length: 15 }, - newText: '\t\tthis.missingThis', - }], - }], - }).reduce(applyReducer, null as any).toPromise() + it('should apply a WorkspaceEdit for the passed FileTextChanges', async function( + this: TestContext & ITestCallbackContext + ): Promise { + await this.service + .workspaceExecuteCommand({ + command: 'codeFix', + arguments: [ + { + fileName: uri2path(rootUri + 'a.ts'), + textChanges: [ + { + span: { start: 50, length: 15 }, + newText: '\t\tthis.missingThis', + }, + ], + }, + ], + }) + .reduce(applyReducer, null as any) + .toPromise() sinon.assert.calledOnce(this.client.workspaceApplyEdit) const workspaceEdit = this.client.workspaceApplyEdit.lastCall.args[0] assert.deepEqual(workspaceEdit, { edit: { changes: { - [rootUri + 'a.ts']: [{ - newText: '\t\tthis.missingThis', - range: { - end: { - character: 9, - line: 5, - }, - start: { - character: 0, - line: 3, + [rootUri + 'a.ts']: [ + { + newText: '\t\tthis.missingThis', + range: { + end: { + character: 9, + line: 5, + }, + start: { + character: 0, + line: 3, + }, }, }, - }], + ], }, }, }) @@ -2942,27 +3389,37 @@ export function describeTypeScriptService(createService: TypeScriptServiceFactor }) describe('Special file names', function(this: TestContext & ISuiteCallbackContext): void { - - beforeEach(initializeTypeScriptService(createService, rootUri, new Map([ - [rootUri + 'keywords-in-path/class/constructor/a.ts', 'export function a() {}'], - [rootUri + 'special-characters-in-path/%40foo/b.ts', 'export function b() {}'], - [rootUri + 'windows/app/master.ts', '/// \nc();'], - [rootUri + 'windows/lib/master.ts', '/// '], - [rootUri + 'windows/lib/slave.ts', 'function c() {}'], - ]))) + beforeEach( + initializeTypeScriptService( + createService, + rootUri, + new Map([ + [rootUri + 'keywords-in-path/class/constructor/a.ts', 'export function a() {}'], + [rootUri + 'special-characters-in-path/%40foo/b.ts', 'export function b() {}'], + [rootUri + 'windows/app/master.ts', '/// \nc();'], + [rootUri + 'windows/lib/master.ts', '/// '], + [rootUri + 'windows/lib/slave.ts', 'function c() {}'], + ]) + ) + ) afterEach(shutdownService) - it('should accept files with TypeScript keywords in path', async function(this: TestContext & ITestCallbackContext): Promise { - const result: Hover = await this.service.textDocumentHover({ - textDocument: { - uri: rootUri + 'keywords-in-path/class/constructor/a.ts', - }, - position: { - line: 0, - character: 16, - }, - }).reduce(applyReducer, null as any).toPromise() + it('should accept files with TypeScript keywords in path', async function( + this: TestContext & ITestCallbackContext + ): Promise { + const result: Hover = await this.service + .textDocumentHover({ + textDocument: { + uri: rootUri + 'keywords-in-path/class/constructor/a.ts', + }, + position: { + line: 0, + character: 16, + }, + }) + .reduce(applyReducer, null as any) + .toPromise() assert.deepEqual(result, { range: { start: { @@ -2974,22 +3431,24 @@ export function describeTypeScriptService(createService: TypeScriptServiceFactor character: 17, }, }, - contents: [ - { language: 'typescript', value: 'function a(): void' }, - '**function** _(exported)_', - ], + contents: [{ language: 'typescript', value: 'function a(): void' }, '**function** _(exported)_'], }) }) - it('should accept files with special characters in path', async function(this: TestContext & ITestCallbackContext): Promise { - const result: Hover = await this.service.textDocumentHover({ - textDocument: { - uri: rootUri + 'special-characters-in-path/%40foo/b.ts', - }, - position: { - line: 0, - character: 16, - }, - }).reduce(applyReducer, null as any).toPromise() + it('should accept files with special characters in path', async function( + this: TestContext & ITestCallbackContext + ): Promise { + const result: Hover = await this.service + .textDocumentHover({ + textDocument: { + uri: rootUri + 'special-characters-in-path/%40foo/b.ts', + }, + position: { + line: 0, + character: 16, + }, + }) + .reduce(applyReducer, null as any) + .toPromise() assert.deepEqual(result, { range: { start: { @@ -3001,35 +3460,39 @@ export function describeTypeScriptService(createService: TypeScriptServiceFactor character: 17, }, }, - contents: [ - { language: 'typescript', value: 'function b(): void' }, - '**function** _(exported)_', - ], + contents: [{ language: 'typescript', value: 'function b(): void' }, '**function** _(exported)_'], }) }) - it('should handle Windows-style paths in triple slash references', async function(this: TestContext & ITestCallbackContext): Promise { - const result = await this.service.textDocumentDefinition({ - textDocument: { - uri: rootUri + 'windows/app/master.ts', - }, - position: { - line: 1, - character: 0, - }, - }).reduce(applyReducer, null as any).toPromise() - assert.deepEqual(result, [{ - range: { - start: { - line: 0, - character: 9, + it('should handle Windows-style paths in triple slash references', async function( + this: TestContext & ITestCallbackContext + ): Promise { + const result = await this.service + .textDocumentDefinition({ + textDocument: { + uri: rootUri + 'windows/app/master.ts', }, - end: { - line: 0, - character: 10, + position: { + line: 1, + character: 0, + }, + }) + .reduce(applyReducer, null as any) + .toPromise() + assert.deepEqual(result, [ + { + range: { + start: { + line: 0, + character: 9, + }, + end: { + line: 0, + character: 10, + }, }, + uri: rootUri + 'windows/lib/slave.ts', }, - uri: rootUri + 'windows/lib/slave.ts', - }]) + ]) }) }) } diff --git a/src/test/typescript-service.test.ts b/src/test/typescript-service.test.ts index 4bc11ae48..986efc002 100644 --- a/src/test/typescript-service.test.ts +++ b/src/test/typescript-service.test.ts @@ -1,4 +1,3 @@ - import { TypeScriptService } from '../typescript-service' import { describeTypeScriptService } from './typescript-service-helpers' diff --git a/src/test/util-test.ts b/src/test/util-test.ts index 7f0bc8d09..cba75ba4a 100644 --- a/src/test/util-test.ts +++ b/src/test/util-test.ts @@ -1,5 +1,13 @@ import * as assert from 'assert' -import { getMatchingPropertyCount, getPropertyCount, isGlobalTSFile, isSymbolDescriptorMatch, JSONPTR, path2uri, uri2path } from '../util' +import { + getMatchingPropertyCount, + getPropertyCount, + isGlobalTSFile, + isSymbolDescriptorMatch, + JSONPTR, + path2uri, + uri2path, +} from '../util' describe('util', () => { describe('JSONPTR', () => { @@ -11,57 +19,69 @@ describe('util', () => { }) describe('getMatchingPropertyCount()', () => { it('should return a score of 4 if 4 properties match', () => { - const score = getMatchingPropertyCount({ - containerName: 'ts', - kind: 'interface', - name: 'Program', - package: undefined, - }, { - containerKind: 'module', - containerName: 'ts', - kind: 'interface', - name: 'Program', - package: undefined, - }) + const score = getMatchingPropertyCount( + { + containerName: 'ts', + kind: 'interface', + name: 'Program', + package: undefined, + }, + { + containerKind: 'module', + containerName: 'ts', + kind: 'interface', + name: 'Program', + package: undefined, + } + ) assert.equal(score, 4) }) it('should return a score of 0.6 if a string property is 60% similar', () => { - const score = getMatchingPropertyCount({ - filePath: 'lib/foo.d.ts', - }, { - filePath: 'src/foo.ts', - }) + const score = getMatchingPropertyCount( + { + filePath: 'lib/foo.d.ts', + }, + { + filePath: 'src/foo.ts', + } + ) assert.equal(score, 0.6) }) it('should return a score of 4 if 4 properties match and 1 does not', () => { - const score = getMatchingPropertyCount({ - containerKind: '', - containerName: 'util', - kind: 'var', - name: 'colors', - package: undefined, - }, { - containerKind: '', - containerName: '', - kind: 'var', - name: 'colors', - package: undefined, - }) + const score = getMatchingPropertyCount( + { + containerKind: '', + containerName: 'util', + kind: 'var', + name: 'colors', + package: undefined, + }, + { + containerKind: '', + containerName: '', + kind: 'var', + name: 'colors', + package: undefined, + } + ) assert.equal(score, 4) }) it('should return a score of 3 if 3 properties match deeply', () => { - const score = getMatchingPropertyCount({ - name: 'a', - kind: 'class', - package: { name: 'mypkg' }, - containerKind: undefined, - }, { - kind: 'class', - name: 'a', - containerKind: '', - containerName: '', - package: { name: 'mypkg' }, - }) + const score = getMatchingPropertyCount( + { + name: 'a', + kind: 'class', + package: { name: 'mypkg' }, + containerKind: undefined, + }, + { + kind: 'class', + name: 'a', + containerKind: '', + containerName: '', + package: { name: 'mypkg' }, + } + ) assert.equal(score, 3) }) }) @@ -80,38 +100,44 @@ describe('util', () => { }) describe('isSymbolDescriptorMatch()', () => { it('should return true for a matching query', () => { - const matches = isSymbolDescriptorMatch({ - containerKind: undefined, - containerName: 'ts', - kind: 'interface', - name: 'Program', - filePath: 'foo/bar.ts', - package: undefined, - }, { - containerKind: 'module', - containerName: 'ts', - kind: 'interface', - name: 'Program', - filePath: 'foo/bar.ts', - package: undefined, - }) + const matches = isSymbolDescriptorMatch( + { + containerKind: undefined, + containerName: 'ts', + kind: 'interface', + name: 'Program', + filePath: 'foo/bar.ts', + package: undefined, + }, + { + containerKind: 'module', + containerName: 'ts', + kind: 'interface', + name: 'Program', + filePath: 'foo/bar.ts', + package: undefined, + } + ) assert.equal(matches, true) }) it('should return true for a matching query with PackageDescriptor', () => { - const matches = isSymbolDescriptorMatch({ - name: 'a', - kind: 'class', - package: { name: 'mypkg' }, - filePath: 'foo/bar.ts', - containerKind: undefined, - }, { - kind: 'class', - name: 'a', - containerKind: '', - containerName: '', - filePath: 'foo/bar.ts', - package: { name: 'mypkg' }, - }) + const matches = isSymbolDescriptorMatch( + { + name: 'a', + kind: 'class', + package: { name: 'mypkg' }, + filePath: 'foo/bar.ts', + containerKind: undefined, + }, + { + kind: 'class', + name: 'a', + containerKind: '', + containerName: '', + filePath: 'foo/bar.ts', + package: { name: 'mypkg' }, + } + ) assert.equal(matches, true) }) }) diff --git a/src/tracing.ts b/src/tracing.ts index 2d69773ae..63e067381 100644 --- a/src/tracing.ts +++ b/src/tracing.ts @@ -1,4 +1,3 @@ - import { Span } from 'opentracing' import { Observable } from 'rxjs' @@ -17,7 +16,7 @@ export function traceSync(operationName: string, childOf: Span, operation: (s return operation(span) } catch (err) { span.setTag('error', true) - span.log({ 'event': 'error', 'error.object': err, 'stack': err.stack, 'message': err.message }) + span.log({ event: 'error', 'error.object': err, stack: err.stack, message: err.message }) throw err } finally { span.finish() @@ -33,13 +32,17 @@ export function traceSync(operationName: string, childOf: Span, operation: (s * @param childOf The parent span * @param operation The function to call */ -export async function tracePromise(operationName: string, childOf: Span, operation: (span: Span) => Promise): Promise { +export async function tracePromise( + operationName: string, + childOf: Span, + operation: (span: Span) => Promise +): Promise { const span = childOf.tracer().startSpan(operationName, { childOf }) try { return await operation(span) } catch (err) { span.setTag('error', true) - span.log({ 'event': 'error', 'error.object': err, 'stack': err.stack, 'message': err.message }) + span.log({ event: 'error', 'error.object': err, stack: err.stack, message: err.message }) throw err } finally { span.finish() @@ -55,20 +58,24 @@ export async function tracePromise(operationName: string, childOf: Span, oper * @param childOf The parent span * @param operation The function to call */ -export function traceObservable(operationName: string, childOf: Span, operation: (span: Span) => Observable): Observable { +export function traceObservable( + operationName: string, + childOf: Span, + operation: (span: Span) => Observable +): Observable { const span = childOf.tracer().startSpan(operationName, { childOf }) try { return operation(span) .do(undefined as any, err => { span.setTag('error', true) - span.log({ 'event': 'error', 'error.object': err, 'stack': err.stack, 'message': err.message }) + span.log({ event: 'error', 'error.object': err, stack: err.stack, message: err.message }) }) .finally(() => { span.finish() }) } catch (err) { span.setTag('error', true) - span.log({ 'event': 'error', 'error.object': err, 'stack': err.stack, 'message': err.message }) + span.log({ event: 'error', 'error.object': err, stack: err.stack, message: err.message }) span.finish() return Observable.throw(err) } diff --git a/src/typescript-service.ts b/src/typescript-service.ts index 9d91f5927..6dcb7878f 100644 --- a/src/typescript-service.ts +++ b/src/typescript-service.ts @@ -40,7 +40,13 @@ import { FileSystem, FileSystemUpdater, LocalFileSystem, RemoteFileSystem } from import { LanguageClient } from './lang-handler' import { Logger, LSPLogger } from './logging' import { InMemoryFileSystem, isTypeScriptLibrary } from './memfs' -import { DEPENDENCY_KEYS, extractDefinitelyTypedPackageName, extractNodeModulesPackageName, PackageJson, PackageManager } from './packages' +import { + DEPENDENCY_KEYS, + extractDefinitelyTypedPackageName, + extractNodeModulesPackageName, + PackageJson, + PackageManager, +} from './packages' import { ProjectConfiguration, ProjectManager } from './project-manager' import { CompletionItem, @@ -127,7 +133,6 @@ const completionKinds: { [name: string]: CompletionItemKind } = { * underscore. */ export class TypeScriptService { - public projectManager: ProjectManager /** @@ -236,16 +241,20 @@ export class TypeScriptService { this.rootUri = params.rootUri || path2uri(params.rootPath!) // tslint:enable:deprecation - this.supportsCompletionWithSnippets = params.capabilities.textDocument && - params.capabilities.textDocument.completion && - params.capabilities.textDocument.completion.completionItem && - params.capabilities.textDocument.completion.completionItem.snippetSupport || false + this.supportsCompletionWithSnippets = + (params.capabilities.textDocument && + params.capabilities.textDocument.completion && + params.capabilities.textDocument.completion.completionItem && + params.capabilities.textDocument.completion.completionItem.snippetSupport) || + false // The root URI always refers to a directory if (!this.rootUri.endsWith('/')) { this.rootUri += '/' } - this._initializeFileSystems(!this.options.strict && !(params.capabilities.xcontentProvider && params.capabilities.xfilesProvider)) + this._initializeFileSystems( + !this.options.strict && !(params.capabilities.xcontentProvider && params.capabilities.xfilesProvider) + ) this.updater = new FileSystemUpdater(this.fileSystem, this.inMemoryFileSystem) this.projectManager = new ProjectManager( this.root, @@ -365,7 +374,8 @@ export class TypeScriptService { const uri = normalizeUri(params.textDocument.uri) // Fetch files needed to resolve definition - return this.projectManager.ensureReferencedFiles(uri, undefined, undefined, span) + return this.projectManager + .ensureReferencedFiles(uri, undefined, undefined, span) .toArray() .mergeMap(() => { const fileName: string = uri2path(uri) @@ -377,25 +387,33 @@ export class TypeScriptService { throw new Error(`Expected source file ${fileName} to exist`) } - const offset: number = ts.getPositionOfLineAndCharacter(sourceFile, params.position.line, params.position.character) - const definitions: ts.DefinitionInfo[] | undefined = configuration.getService().getDefinitionAtPosition(fileName, offset) - - return Observable.from(definitions || []) - .map((definition): Location => { - const sourceFile = this._getSourceFile(configuration, definition.fileName, span) - if (!sourceFile) { - throw new Error('expected source file "' + definition.fileName + '" to exist in configuration') - } - const start = ts.getLineAndCharacterOfPosition(sourceFile, definition.textSpan.start) - const end = ts.getLineAndCharacterOfPosition(sourceFile, definition.textSpan.start + definition.textSpan.length) - return { - uri: locationUri(definition.fileName), - range: { - start, - end, - }, - } - }) + const offset: number = ts.getPositionOfLineAndCharacter( + sourceFile, + params.position.line, + params.position.character + ) + const definitions: ts.DefinitionInfo[] | undefined = configuration + .getService() + .getDefinitionAtPosition(fileName, offset) + + return Observable.from(definitions || []).map((definition): Location => { + const sourceFile = this._getSourceFile(configuration, definition.fileName, span) + if (!sourceFile) { + throw new Error('expected source file "' + definition.fileName + '" to exist in configuration') + } + const start = ts.getLineAndCharacterOfPosition(sourceFile, definition.textSpan.start) + const end = ts.getLineAndCharacterOfPosition( + sourceFile, + definition.textSpan.start + definition.textSpan.length + ) + return { + uri: locationUri(definition.fileName), + range: { + start, + end, + }, + } + }) }) } @@ -421,10 +439,14 @@ export class TypeScriptService { /** * Returns an Observable of SymbolLocationInformations for the definition of a symbol at the given position */ - protected _getSymbolLocationInformations(params: TextDocumentPositionParams, span = new Span()): Observable { + protected _getSymbolLocationInformations( + params: TextDocumentPositionParams, + span = new Span() + ): Observable { const uri = normalizeUri(params.textDocument.uri) // Ensure files needed to resolve SymbolLocationInformation are fetched - return this.projectManager.ensureReferencedFiles(uri, undefined, undefined, span) + return this.projectManager + .ensureReferencedFiles(uri, undefined, undefined, span) .toArray() .mergeMap(() => { // Convert URI to file path @@ -437,35 +459,43 @@ export class TypeScriptService { throw new Error(`Unknown text document ${uri}`) } // Convert line/character to offset - const offset: number = ts.getPositionOfLineAndCharacter(sourceFile, params.position.line, params.position.character) + const offset: number = ts.getPositionOfLineAndCharacter( + sourceFile, + params.position.line, + params.position.character + ) // Query TypeScript for references - return Observable.from(configuration.getService().getDefinitionAtPosition(fileName, offset) || []) - .mergeMap((definition: ts.DefinitionInfo): Observable => { - const definitionUri = locationUri(definition.fileName) - // Get the PackageDescriptor - return this._getPackageDescriptor(definitionUri) - .defaultIfEmpty(undefined) - .map((packageDescriptor: PackageDescriptor | undefined): SymbolLocationInformation => { - const sourceFile = this._getSourceFile(configuration, definition.fileName, span) - if (!sourceFile) { - throw new Error(`Expected source file ${definition.fileName} to exist in configuration`) - } - const symbol = definitionInfoToSymbolDescriptor(definition, this.root) - if (packageDescriptor) { - symbol.package = packageDescriptor - } - return { - symbol, - location: { - uri: definitionUri, - range: { - start: ts.getLineAndCharacterOfPosition(sourceFile, definition.textSpan.start), - end: ts.getLineAndCharacterOfPosition(sourceFile, definition.textSpan.start + definition.textSpan.length), - }, + return Observable.from( + configuration.getService().getDefinitionAtPosition(fileName, offset) || [] + ).mergeMap((definition: ts.DefinitionInfo): Observable => { + const definitionUri = locationUri(definition.fileName) + // Get the PackageDescriptor + return this._getPackageDescriptor(definitionUri) + .defaultIfEmpty(undefined) + .map((packageDescriptor: PackageDescriptor | undefined): SymbolLocationInformation => { + const sourceFile = this._getSourceFile(configuration, definition.fileName, span) + if (!sourceFile) { + throw new Error(`Expected source file ${definition.fileName} to exist in configuration`) + } + const symbol = definitionInfoToSymbolDescriptor(definition, this.root) + if (packageDescriptor) { + symbol.package = packageDescriptor + } + return { + symbol, + location: { + uri: definitionUri, + range: { + start: ts.getLineAndCharacterOfPosition(sourceFile, definition.textSpan.start), + end: ts.getLineAndCharacterOfPosition( + sourceFile, + definition.textSpan.start + definition.textSpan.length + ), }, - } - }) - }) + }, + } + }) + }) }) } @@ -482,15 +512,20 @@ export class TypeScriptService { if (packageName) { // The symbol is part of a dependency in node_modules // Build URI to package.json of the Dependency - const encodedPackageName = packageName.split('/').map(encodeURIComponent).join('/') + const encodedPackageName = packageName + .split('/') + .map(encodeURIComponent) + .join('/') const parts: url.UrlObject = url.parse(uri) const packageJsonUri = url.format({ ...parts, - pathname: parts.pathname!.slice(0, parts.pathname!.lastIndexOf('/node_modules/' + encodedPackageName)) + `/node_modules/${encodedPackageName}/package.json`, + pathname: + parts.pathname!.slice(0, parts.pathname!.lastIndexOf('/node_modules/' + encodedPackageName)) + + `/node_modules/${encodedPackageName}/package.json`, }) // Fetch the package.json of the dependency - return this.updater.ensure(packageJsonUri, span) - .concat(Observable.defer((): Observable => { + return this.updater.ensure(packageJsonUri, span).concat( + Observable.defer((): Observable => { const packageJson: PackageJson = JSON.parse(this.inMemoryFileSystem.getContent(packageJsonUri)) const { name, version } = packageJson if (!name) { @@ -503,32 +538,33 @@ export class TypeScriptService { repoURL = 'https://github.com/DefinitelyTyped/DefinitelyTyped' } else { // else use repository field from package.json - repoURL = typeof packageJson.repository === 'object' ? packageJson.repository.url : undefined + repoURL = + typeof packageJson.repository === 'object' ? packageJson.repository.url : undefined } return Observable.of({ name, version, repoURL }) - })) + }) + ) } else { // The symbol is defined in the root package of the workspace, not in a dependency // Get root package.json - return this.packageManager.getClosestPackageJson(uri, span) - .mergeMap(packageJson => { - let { name, version } = packageJson + return this.packageManager.getClosestPackageJson(uri, span).mergeMap(packageJson => { + let { name, version } = packageJson + if (!name) { + return [] + } + let repoURL = typeof packageJson.repository === 'object' ? packageJson.repository.url : undefined + // If the root package is DefinitelyTyped, find out the proper @types package name for each typing + if (name === 'definitely-typed') { + name = extractDefinitelyTypedPackageName(uri) if (!name) { + this.logger.error(`Could not extract package name from DefinitelyTyped URI ${uri}`) return [] } - let repoURL = typeof packageJson.repository === 'object' ? packageJson.repository.url : undefined - // If the root package is DefinitelyTyped, find out the proper @types package name for each typing - if (name === 'definitely-typed') { - name = extractDefinitelyTypedPackageName(uri) - if (!name) { - this.logger.error(`Could not extract package name from DefinitelyTyped URI ${uri}`) - return [] - } - version = undefined - repoURL = 'https://github.com/DefinitelyTyped/DefinitelyTyped' - } - return [{ name, version, repoURL } as PackageDescriptor] - }) + version = undefined + repoURL = 'https://github.com/DefinitelyTyped/DefinitelyTyped' + } + return [{ name, version, repoURL } as PackageDescriptor] + }) } }) } @@ -540,8 +576,7 @@ export class TypeScriptService { * @return Observable of JSON Patches that build a `Hover` result */ public textDocumentHover(params: TextDocumentPositionParams, span = new Span()): Observable { - return this._getHover(params, span) - .map(hover => ({ op: 'add', path: '', value: hover }) as Operation) + return this._getHover(params, span).map(hover => ({ op: 'add', path: '', value: hover } as Operation)) } /** @@ -551,7 +586,8 @@ export class TypeScriptService { const uri = normalizeUri(params.textDocument.uri) // Ensure files needed to resolve hover are fetched - return this.projectManager.ensureReferencedFiles(uri, undefined, undefined, span) + return this.projectManager + .ensureReferencedFiles(uri, undefined, undefined, span) .toArray() .map((): Hover => { const fileName: string = uri2path(uri) @@ -562,7 +598,11 @@ export class TypeScriptService { if (!sourceFile) { throw new Error(`Unknown text document ${uri}`) } - const offset: number = ts.getPositionOfLineAndCharacter(sourceFile, params.position.line, params.position.character) + const offset: number = ts.getPositionOfLineAndCharacter( + sourceFile, + params.position.line, + params.position.character + ) const info = configuration.getService().getQuickInfoAtPosition(fileName, offset) if (!info) { return { contents: [] } @@ -577,16 +617,21 @@ export class TypeScriptService { const modifiers = info.kindModifiers .split(',') // Filter out some quirks like "constructor (exported)" - .filter(mod => mod && ( - mod !== ts.ScriptElementKindModifier.exportedModifier - || info.kind !== ts.ScriptElementKind.constructorImplementationElement - )) + .filter( + mod => + mod && + (mod !== ts.ScriptElementKindModifier.exportedModifier || + info.kind !== ts.ScriptElementKind.constructorImplementationElement) + ) // Make proper adjectives .map(mod => { switch (mod) { - case ts.ScriptElementKindModifier.ambientModifier: return 'ambient' - case ts.ScriptElementKindModifier.exportedModifier: return 'exported' - default: return mod + case ts.ScriptElementKindModifier.ambientModifier: + return 'ambient' + case ts.ScriptElementKindModifier.exportedModifier: + return 'exported' + default: + return mod } }) if (modifiers.length > 0) { @@ -624,54 +669,70 @@ export class TypeScriptService { const uri = normalizeUri(params.textDocument.uri) // Ensure all files were fetched to collect all references - return this.projectManager.ensureOwnFiles(span) - .concat(Observable.defer(() => { - // Convert URI to file path because TypeScript doesn't work with URIs - const fileName = uri2path(uri) - // Get tsconfig configuration for requested file - const configuration = this.projectManager.getConfiguration(fileName) - // Ensure all files have been added - configuration.ensureAllFiles(span) - const program = configuration.getProgram(span) - if (!program) { - return Observable.empty() - } - // Get SourceFile object for requested file - const sourceFile = this._getSourceFile(configuration, fileName, span) - if (!sourceFile) { - throw new Error(`Source file ${fileName} does not exist`) - } - // Convert line/character to offset - const offset: number = ts.getPositionOfLineAndCharacter(sourceFile, params.position.line, params.position.character) - // Request references at position from TypeScript - // Despite the signature, getReferencesAtPosition() can return undefined - return Observable.from(configuration.getService().getReferencesAtPosition(fileName, offset) || []) - .filter(reference => - // Filter declaration if not requested - (!reference.isDefinition || (params.context && params.context.includeDeclaration)) - // Filter references in node_modules - && !reference.fileName.includes('/node_modules/') - ) - .map((reference): Location => { - const sourceFile = program.getSourceFile(reference.fileName) - if (!sourceFile) { - throw new Error(`Source file ${reference.fileName} does not exist`) + return ( + this.projectManager + .ensureOwnFiles(span) + .concat( + Observable.defer(() => { + // Convert URI to file path because TypeScript doesn't work with URIs + const fileName = uri2path(uri) + // Get tsconfig configuration for requested file + const configuration = this.projectManager.getConfiguration(fileName) + // Ensure all files have been added + configuration.ensureAllFiles(span) + const program = configuration.getProgram(span) + if (!program) { + return Observable.empty() } - // Convert offset to line/character position - const start = ts.getLineAndCharacterOfPosition(sourceFile, reference.textSpan.start) - const end = ts.getLineAndCharacterOfPosition(sourceFile, reference.textSpan.start + reference.textSpan.length) - return { - uri: path2uri(reference.fileName), - range: { - start, - end, - }, + // Get SourceFile object for requested file + const sourceFile = this._getSourceFile(configuration, fileName, span) + if (!sourceFile) { + throw new Error(`Source file ${fileName} does not exist`) } + // Convert line/character to offset + const offset: number = ts.getPositionOfLineAndCharacter( + sourceFile, + params.position.line, + params.position.character + ) + // Request references at position from TypeScript + // Despite the signature, getReferencesAtPosition() can return undefined + return Observable.from( + configuration.getService().getReferencesAtPosition(fileName, offset) || [] + ) + .filter( + reference => + // Filter declaration if not requested + (!reference.isDefinition || + (params.context && params.context.includeDeclaration)) && + // Filter references in node_modules + !reference.fileName.includes('/node_modules/') + ) + .map((reference): Location => { + const sourceFile = program.getSourceFile(reference.fileName) + if (!sourceFile) { + throw new Error(`Source file ${reference.fileName} does not exist`) + } + // Convert offset to line/character position + const start = ts.getLineAndCharacterOfPosition(sourceFile, reference.textSpan.start) + const end = ts.getLineAndCharacterOfPosition( + sourceFile, + reference.textSpan.start + reference.textSpan.length + ) + return { + uri: path2uri(reference.fileName), + range: { + start, + end, + }, + } + }) }) - })) - .map((location: Location): Operation => ({ op: 'add', path: '/-', value: location })) - // Initialize with array - .startWith({ op: 'add', path: '', value: [] }) + ) + .map((location: Location): Operation => ({ op: 'add', path: '/-', value: location })) + // Initialize with array + .startWith({ op: 'add', path: '', value: [] }) + ) } /** @@ -682,7 +743,6 @@ export class TypeScriptService { * @return Observable of JSON Patches that build a `SymbolInformation[]` result */ public workspaceSymbol(params: WorkspaceSymbolParams, span = new Span()): Observable { - // Return cached result for empty query, if available if (!params.query && !params.symbol && this.emptyQueryWorkspaceSymbols) { return this.emptyQueryWorkspaceSymbols @@ -697,8 +757,17 @@ export class TypeScriptService { // Search only in the correct subdirectory for the given PackageDescriptor if (isDefinitelyTyped) { // Error if not passed a SymbolDescriptor query with an `@types` PackageDescriptor - if (!params.symbol || !params.symbol.package || !params.symbol.package.name || !params.symbol.package.name.startsWith('@types/')) { - return Observable.throw(new Error('workspace/symbol on DefinitelyTyped is only supported with a SymbolDescriptor query with an @types PackageDescriptor')) + if ( + !params.symbol || + !params.symbol.package || + !params.symbol.package.name || + !params.symbol.package.name.startsWith('@types/') + ) { + return Observable.throw( + new Error( + 'workspace/symbol on DefinitelyTyped is only supported with a SymbolDescriptor query with an @types PackageDescriptor' + ) + ) } // Fetch all files in the package subdirectory @@ -706,40 +775,63 @@ export class TypeScriptService { const normRootUri = this.rootUri.endsWith('/') ? this.rootUri : this.rootUri + '/' const packageRootUri = normRootUri + params.symbol.package.name.substr(1) + '/' - return this.updater.ensureStructure(span) + return this.updater + .ensureStructure(span) .concat(Observable.defer(() => observableFromIterable(this.inMemoryFileSystem.uris()))) .filter(uri => uri.startsWith(packageRootUri)) .mergeMap(uri => this.updater.ensure(uri, span)) - .concat(Observable.defer(() => { - span.log({ event: 'fetched package files' }) - const config = this.projectManager.getParentConfiguration(packageRootUri, 'ts') - if (!config) { - throw new Error(`Could not find tsconfig for ${packageRootUri}`) - } - // Don't match PackageDescriptor on symbols - return this._getSymbolsInConfig(config, omit(params.symbol!, 'package'), span) - })) + .concat( + Observable.defer(() => { + span.log({ event: 'fetched package files' }) + const config = this.projectManager.getParentConfiguration(packageRootUri, 'ts') + if (!config) { + throw new Error(`Could not find tsconfig for ${packageRootUri}`) + } + // Don't match PackageDescriptor on symbols + return this._getSymbolsInConfig(config, omit(params.symbol!, 'package'), span) + }) + ) } // Regular workspace symbol search // Search all symbols in own code, but not in dependencies - return this.projectManager.ensureOwnFiles(span) - .concat(Observable.defer(() => { - if (params.symbol && params.symbol.package && params.symbol.package.name) { - // If SymbolDescriptor query with PackageDescriptor, search for package.jsons with matching package name - return observableFromIterable(this.packageManager.packageJsonUris()) - .filter(packageJsonUri => (JSON.parse(this.inMemoryFileSystem.getContent(packageJsonUri)) as PackageJson).name === params.symbol!.package!.name) - // Find their parent and child tsconfigs - .mergeMap(packageJsonUri => Observable.merge( - castArray(this.projectManager.getParentConfiguration(packageJsonUri) || []), - // Search child directories starting at the directory of the package.json - observableFromIterable(this.projectManager.getChildConfigurations(url.resolve(packageJsonUri, '.'))) - )) - } - // Else search all tsconfigs in the workspace - return observableFromIterable(this.projectManager.configurations()) - })) - // If PackageDescriptor is given, only search project with the matching package name - .mergeMap(config => this._getSymbolsInConfig(config, params.query || params.symbol, span)) + return ( + this.projectManager + .ensureOwnFiles(span) + .concat( + Observable.defer(() => { + if (params.symbol && params.symbol.package && params.symbol.package.name) { + // If SymbolDescriptor query with PackageDescriptor, search for package.jsons with matching package name + return ( + observableFromIterable(this.packageManager.packageJsonUris()) + .filter( + packageJsonUri => + (JSON.parse( + this.inMemoryFileSystem.getContent(packageJsonUri) + ) as PackageJson).name === params.symbol!.package!.name + ) + // Find their parent and child tsconfigs + .mergeMap(packageJsonUri => + Observable.merge( + castArray( + this.projectManager.getParentConfiguration(packageJsonUri) || [] + ), + // Search child directories starting at the directory of the package.json + observableFromIterable( + this.projectManager.getChildConfigurations( + url.resolve(packageJsonUri, '.') + ) + ) + ) + ) + ) + } + // Else search all tsconfigs in the workspace + return observableFromIterable(this.projectManager.configurations()) + }) + ) + // If PackageDescriptor is given, only search project with the matching package name + .mergeMap(config => this._getSymbolsInConfig(config, params.query || params.symbol, span)) + ) }) // Filter duplicate symbols // There may be few configurations that contain the same file(s) @@ -777,7 +869,8 @@ export class TypeScriptService { const uri = normalizeUri(params.textDocument.uri) // Ensure files needed to resolve symbols are fetched - return this.projectManager.ensureReferencedFiles(uri, undefined, undefined, span) + return this.projectManager + .ensureReferencedFiles(uri, undefined, undefined, span) .toArray() .mergeMap(() => { const fileName = uri2path(uri) @@ -793,7 +886,7 @@ export class TypeScriptService { .filter(({ tree, parent }) => navigationTreeIsSymbol(tree)) .map(({ tree, parent }) => navigationTreeToSymbolInformation(tree, parent, sourceFile, this.root)) }) - .map(symbol => ({ op: 'add', path: '/-', value: symbol }) as Operation) + .map(symbol => ({ op: 'add', path: '/-', value: symbol } as Operation)) .startWith({ op: 'add', path: '', value: [] } as Operation) } @@ -813,84 +906,115 @@ export class TypeScriptService { } return this.projectManager.ensureAllFiles(span) }) - .concat(Observable.defer(() => { - // if we were hinted that we should only search a specific package, find it and only search the owning tsconfig.json - if (params.hints && params.hints.dependeePackageName) { - return observableFromIterable(this.packageManager.packageJsonUris()) - .filter(uri => (JSON.parse(this.inMemoryFileSystem.getContent(uri)) as PackageJson).name === params.hints!.dependeePackageName) - .take(1) - .mergeMap(uri => { - const config = this.projectManager.getParentConfiguration(uri) - if (!config) { - return observableFromIterable(this.projectManager.configurations()) - } - return [config] - }) - } - // else search all tsconfig.jsons - return observableFromIterable(this.projectManager.configurations()) - })) + .concat( + Observable.defer(() => { + // if we were hinted that we should only search a specific package, find it and only search the owning tsconfig.json + if (params.hints && params.hints.dependeePackageName) { + return observableFromIterable(this.packageManager.packageJsonUris()) + .filter( + uri => + (JSON.parse(this.inMemoryFileSystem.getContent(uri)) as PackageJson).name === + params.hints!.dependeePackageName + ) + .take(1) + .mergeMap(uri => { + const config = this.projectManager.getParentConfiguration(uri) + if (!config) { + return observableFromIterable(this.projectManager.configurations()) + } + return [config] + }) + } + // else search all tsconfig.jsons + return observableFromIterable(this.projectManager.configurations()) + }) + ) .mergeMap((config: ProjectConfiguration) => { config.ensureAllFiles(span) const program = config.getProgram(span) if (!program) { return Observable.empty() } - return Observable.from(program.getSourceFiles()) - // Ignore dependency files - .filter(source => !toUnixPath(source.fileName).includes('/node_modules/')) - .mergeMap(source => - // Iterate AST of source file - observableFromIterable(walkMostAST(source)) - // Filter Identifier Nodes - // TODO: include string-interpolated references - .filter((node): node is ts.Identifier => node.kind === ts.SyntaxKind.Identifier) - .mergeMap(node => { - try { - // Find definition for node - return Observable.from(config.getService().getDefinitionAtPosition(source.fileName, node.pos + 1) || []) - .mergeMap(definition => { - const symbol = definitionInfoToSymbolDescriptor(definition, this.root) - // Check if SymbolDescriptor without PackageDescriptor matches - const score = getMatchingPropertyCount(queryWithoutPackage, symbol) - if (score < minScore || (params.query.package && !definition.fileName.includes(params.query.package.name))) { - return [] - } - span.log({ event: 'match', score }) - // If no PackageDescriptor query, return match - if (!params.query.package || !params.query.package) { - return [symbol] - } - // If SymbolDescriptor matched and the query contains a PackageDescriptor, get package.json and match PackageDescriptor name - // TODO match full PackageDescriptor (version) and fill out the symbol.package field - const uri = path2uri(definition.fileName) - return this._getPackageDescriptor(uri, span) - .defaultIfEmpty(undefined) - .filter(packageDescriptor => !!(packageDescriptor && packageDescriptor.name === params.query.package!.name!)) - .map(packageDescriptor => { - symbol.package = packageDescriptor - return symbol - }) - }) - .map((symbol: SymbolDescriptor): ReferenceInformation => ({ - symbol, - reference: { - uri: locationUri(source.fileName), - range: { - start: ts.getLineAndCharacterOfPosition(source, node.pos), - end: ts.getLineAndCharacterOfPosition(source, node.end), + return ( + Observable.from(program.getSourceFiles()) + // Ignore dependency files + .filter(source => !toUnixPath(source.fileName).includes('/node_modules/')) + .mergeMap(source => + // Iterate AST of source file + observableFromIterable(walkMostAST(source)) + // Filter Identifier Nodes + // TODO: include string-interpolated references + .filter((node): node is ts.Identifier => node.kind === ts.SyntaxKind.Identifier) + .mergeMap(node => { + try { + // Find definition for node + return Observable.from( + config + .getService() + .getDefinitionAtPosition(source.fileName, node.pos + 1) || [] + ) + .mergeMap(definition => { + const symbol = definitionInfoToSymbolDescriptor(definition, this.root) + // Check if SymbolDescriptor without PackageDescriptor matches + const score = getMatchingPropertyCount(queryWithoutPackage, symbol) + if ( + score < minScore || + (params.query.package && + !definition.fileName.includes(params.query.package.name)) + ) { + return [] + } + span.log({ event: 'match', score }) + // If no PackageDescriptor query, return match + if (!params.query.package || !params.query.package) { + return [symbol] + } + // If SymbolDescriptor matched and the query contains a PackageDescriptor, get package.json and match PackageDescriptor name + // TODO match full PackageDescriptor (version) and fill out the symbol.package field + const uri = path2uri(definition.fileName) + return this._getPackageDescriptor(uri, span) + .defaultIfEmpty(undefined) + .filter( + packageDescriptor => + !!( + packageDescriptor && + packageDescriptor.name === params.query.package!.name! + ) + ) + .map(packageDescriptor => { + symbol.package = packageDescriptor + return symbol + }) + }) + .map((symbol: SymbolDescriptor): ReferenceInformation => ({ + symbol, + reference: { + uri: locationUri(source.fileName), + range: { + start: ts.getLineAndCharacterOfPosition(source, node.pos), + end: ts.getLineAndCharacterOfPosition(source, node.end), + }, }, - }, - })) - } catch (err) { - // Continue with next node on error - // Workaround for https://github.com/Microsoft/TypeScript/issues/15219 - this.logger.error(`workspace/xreferences: Error getting definition for ${source.fileName} at offset ${node.pos + 1}`, err) - span.log({ 'event': 'error', 'error.object': err, 'message': err.message, 'stack': err.stack }) - return [] - } - }) - ) + })) + } catch (err) { + // Continue with next node on error + // Workaround for https://github.com/Microsoft/TypeScript/issues/15219 + this.logger.error( + `workspace/xreferences: Error getting definition for ${source.fileName} at offset ${node.pos + + 1}`, + err + ) + span.log({ + event: 'error', + 'error.object': err, + message: err.message, + stack: err.stack, + }) + return [] + } + }) + ) + ) }) .map((reference): Operation => ({ op: 'add', path: '/-', value: reference })) .startWith({ op: 'add', path: '', value: [] }) @@ -915,58 +1039,67 @@ export class TypeScriptService { // In DefinitelyTyped, report all @types/ packages if (isDefinitelyTyped) { const typesUri = url.resolve(this.rootUri, 'types/') - return observableFromIterable(this.inMemoryFileSystem.uris()) - // Find all types/ subdirectories - .filter(uri => uri.startsWith(typesUri)) - // Get the directory names - .map((uri): PackageInformation => ({ - package: { - name: '@types/' + decodeURIComponent(uri.substr(typesUri.length).split('/')[0]), - // TODO report a version by looking at subfolders like v6 - }, - // TODO parse /// comments in .d.ts files for collecting dependencies between @types packages - dependencies: [], - })) - } - // For other workspaces, search all package.json files - return this.projectManager.ensureModuleStructure(span) - // Iterate all files - .concat(Observable.defer(() => observableFromIterable(this.inMemoryFileSystem.uris()))) - // Filter own package.jsons - .filter(uri => uri.includes('/package.json') && !uri.includes('/node_modules/')) - // Map to contents of package.jsons - .mergeMap(uri => this.packageManager.getPackageJson(uri)) - // Map each package.json to a PackageInformation - .mergeMap(packageJson => { - if (!packageJson.name) { - return [] - } - const packageDescriptor: PackageDescriptor = { - name: packageJson.name, - version: packageJson.version, - repoURL: typeof packageJson.repository === 'object' && packageJson.repository.url || undefined, - } - // Collect all dependencies for this package.json - return Observable.from(DEPENDENCY_KEYS) - .filter(key => !!packageJson[key]) - // Get [name, version] pairs - .mergeMap(key => toPairs(packageJson[key])) - // Map to DependencyReferences - .map(([name, version]): DependencyReference => ({ - attributes: { - name, - version, + return ( + observableFromIterable(this.inMemoryFileSystem.uris()) + // Find all types/ subdirectories + .filter(uri => uri.startsWith(typesUri)) + // Get the directory names + .map((uri): PackageInformation => ({ + package: { + name: '@types/' + decodeURIComponent(uri.substr(typesUri.length).split('/')[0]), + // TODO report a version by looking at subfolders like v6 }, - hints: { - dependeePackageName: packageJson.name, - }, - })) - .toArray() - .map((dependencies): PackageInformation => ({ - package: packageDescriptor, - dependencies, + // TODO parse /// comments in .d.ts files for collecting dependencies between @types packages + dependencies: [], })) - }) + ) + } + // For other workspaces, search all package.json files + return ( + this.projectManager + .ensureModuleStructure(span) + // Iterate all files + .concat(Observable.defer(() => observableFromIterable(this.inMemoryFileSystem.uris()))) + // Filter own package.jsons + .filter(uri => uri.includes('/package.json') && !uri.includes('/node_modules/')) + // Map to contents of package.jsons + .mergeMap(uri => this.packageManager.getPackageJson(uri)) + // Map each package.json to a PackageInformation + .mergeMap(packageJson => { + if (!packageJson.name) { + return [] + } + const packageDescriptor: PackageDescriptor = { + name: packageJson.name, + version: packageJson.version, + repoURL: + (typeof packageJson.repository === 'object' && packageJson.repository.url) || + undefined, + } + // Collect all dependencies for this package.json + return ( + Observable.from(DEPENDENCY_KEYS) + .filter(key => !!packageJson[key]) + // Get [name, version] pairs + .mergeMap(key => toPairs(packageJson[key])) + // Map to DependencyReferences + .map(([name, version]): DependencyReference => ({ + attributes: { + name, + version, + }, + hints: { + dependeePackageName: packageJson.name, + }, + })) + .toArray() + .map((dependencies): PackageInformation => ({ + package: packageDescriptor, + dependencies, + })) + ) + }) + ) }) .map((packageInfo): Operation => ({ op: 'add', path: '/-', value: packageInfo })) .startWith({ op: 'add', path: '', value: [] }) @@ -980,31 +1113,34 @@ export class TypeScriptService { */ public workspaceXdependencies(params = {}, span = new Span()): Observable { // Ensure package.json files - return this.projectManager.ensureModuleStructure() - // Iterate all files - .concat(Observable.defer(() => observableFromIterable(this.inMemoryFileSystem.uris()))) - // Filter own package.jsons - .filter(uri => uri.includes('/package.json') && !uri.includes('/node_modules/')) - // Ensure contents of own package.jsons - .mergeMap(uri => this.packageManager.getPackageJson(uri)) - // Map package.json to DependencyReferences - .mergeMap(packageJson => - Observable.from(DEPENDENCY_KEYS) - .filter(key => !!packageJson[key]) - // Get [name, version] pairs - .mergeMap(key => toPairs(packageJson[key])) - .map(([name, version]): DependencyReference => ({ - attributes: { - name, - version, - }, - hints: { - dependeePackageName: packageJson.name, - }, - })) - ) - .map((dependency): Operation => ({ op: 'add', path: '/-', value: dependency })) - .startWith({ op: 'add', path: '', value: [] }) + return ( + this.projectManager + .ensureModuleStructure() + // Iterate all files + .concat(Observable.defer(() => observableFromIterable(this.inMemoryFileSystem.uris()))) + // Filter own package.jsons + .filter(uri => uri.includes('/package.json') && !uri.includes('/node_modules/')) + // Ensure contents of own package.jsons + .mergeMap(uri => this.packageManager.getPackageJson(uri)) + // Map package.json to DependencyReferences + .mergeMap(packageJson => + Observable.from(DEPENDENCY_KEYS) + .filter(key => !!packageJson[key]) + // Get [name, version] pairs + .mergeMap(key => toPairs(packageJson[key])) + .map(([name, version]): DependencyReference => ({ + attributes: { + name, + version, + }, + hints: { + dependeePackageName: packageJson.name, + }, + })) + ) + .map((dependency): Operation => ({ op: 'add', path: '/-', value: dependency })) + .startWith({ op: 'add', path: '', value: [] }) + ) } /** @@ -1026,10 +1162,10 @@ export class TypeScriptService { const uri = normalizeUri(params.textDocument.uri) // Ensure files needed to suggest completions are fetched - return this.projectManager.ensureReferencedFiles(uri, undefined, undefined, span) + return this.projectManager + .ensureReferencedFiles(uri, undefined, undefined, span) .toArray() .mergeMap(() => { - const fileName: string = uri2path(uri) const configuration = this.projectManager.getConfiguration(fileName) @@ -1040,7 +1176,11 @@ export class TypeScriptService { return [] } - const offset: number = ts.getPositionOfLineAndCharacter(sourceFile, params.position.line, params.position.character) + const offset: number = ts.getPositionOfLineAndCharacter( + sourceFile, + params.position.line, + params.position.character + ) const completions = configuration.getService().getCompletionsAtPosition(fileName, offset) if (!completions) { @@ -1048,7 +1188,7 @@ export class TypeScriptService { } return Observable.from(completions.entries) - .map(entry => { + .map(entry => { const item: CompletionItem = { label: entry.name } const kind = completionKinds[entry.kind] @@ -1083,12 +1223,12 @@ export class TypeScriptService { if (!item.data) { throw new Error('Cannot resolve completion item without data') } - const {uri, offset, entryName} = item.data + const { uri, offset, entryName } = item.data const fileName: string = uri2path(uri) - return this.projectManager.ensureReferencedFiles(uri, undefined, undefined, span) + return this.projectManager + .ensureReferencedFiles(uri, undefined, undefined, span) .toArray() .map(() => { - const configuration = this.projectManager.getConfiguration(fileName) configuration.ensureBasicFiles(span) @@ -1096,8 +1236,10 @@ export class TypeScriptService { if (details) { item.documentation = ts.displayPartsToString(details.documentation) item.detail = ts.displayPartsToString(details.displayParts) - if (this.supportsCompletionWithSnippets && - (details.kind === 'method' || details.kind === 'function')) { + if ( + this.supportsCompletionWithSnippets && + (details.kind === 'method' || details.kind === 'function') + ) { const parameters = details.displayParts .filter(p => p.kind === 'parameterName') // tslint:disable-next-line:no-invalid-template-strings @@ -1113,7 +1255,7 @@ export class TypeScriptService { } return item }) - .map(completionItem => ({ op: 'add', path: '', value: completionItem }) as Operation) + .map(completionItem => ({ op: 'add', path: '', value: completionItem } as Operation)) } /** @@ -1126,10 +1268,10 @@ export class TypeScriptService { const uri = normalizeUri(params.textDocument.uri) // Ensure files needed to resolve signature are fetched - return this.projectManager.ensureReferencedFiles(uri, undefined, undefined, span) + return this.projectManager + .ensureReferencedFiles(uri, undefined, undefined, span) .toArray() .map((): SignatureHelp => { - const filePath = uri2path(uri) const configuration = this.projectManager.getConfiguration(filePath) configuration.ensureBasicFiles(span) @@ -1138,9 +1280,15 @@ export class TypeScriptService { if (!sourceFile) { throw new Error(`expected source file ${filePath} to exist in configuration`) } - const offset: number = ts.getPositionOfLineAndCharacter(sourceFile, params.position.line, params.position.character) - - const signatures: ts.SignatureHelpItems = configuration.getService().getSignatureHelpItems(filePath, offset) + const offset: number = ts.getPositionOfLineAndCharacter( + sourceFile, + params.position.line, + params.position.character + ) + + const signatures: ts.SignatureHelpItems = configuration + .getService() + .getSignatureHelpItems(filePath, offset) if (!signatures) { return { signatures: [], activeParameter: 0, activeSignature: 0 } } @@ -1166,7 +1314,7 @@ export class TypeScriptService { activeParameter: signatures.argumentIndex, } }) - .map(signatureHelp => ({ op: 'add', path: '', value: signatureHelp }) as Operation) + .map(signatureHelp => ({ op: 'add', path: '', value: signatureHelp } as Operation)) } /** @@ -1178,7 +1326,8 @@ export class TypeScriptService { */ public textDocumentCodeAction(params: CodeActionParams, span = new Span()): Observable { const uri = normalizeUri(params.textDocument.uri) - return this.projectManager.ensureReferencedFiles(uri, undefined, undefined, span) + return this.projectManager + .ensureReferencedFiles(uri, undefined, undefined, span) .toArray() .mergeMap(() => { const configuration = this.projectManager.getParentConfiguration(uri) @@ -1193,15 +1342,27 @@ export class TypeScriptService { throw new Error(`Expected source file ${filePath} to exist in configuration`) } - const start = ts.getPositionOfLineAndCharacter(sourceFile, params.range.start.line, params.range.start.character) - const end = ts.getPositionOfLineAndCharacter(sourceFile, params.range.end.line, params.range.end.character) + const start = ts.getPositionOfLineAndCharacter( + sourceFile, + params.range.start.line, + params.range.start.character + ) + const end = ts.getPositionOfLineAndCharacter( + sourceFile, + params.range.end.line, + params.range.end.character + ) const errorCodes = iterate(params.context.diagnostics) .map(diagnostic => diagnostic.code) .filter(code => typeof code === 'number') .toArray() as number[] - return configuration.getService().getCodeFixesAtPosition(filePath, start, end, errorCodes, this.settings.format || {}) || [] + return ( + configuration + .getService() + .getCodeFixesAtPosition(filePath, start, end, errorCodes, this.settings.format || {}) || [] + ) }) .map((action: ts.CodeAction): Operation => ({ op: 'add', @@ -1243,36 +1404,39 @@ export class TypeScriptService { return Observable.throw(new Error('No changes supplied for code fix command')) } - return this.projectManager.ensureOwnFiles(span) - .concat(Observable.defer(() => { - // Configuration lookup uses Windows paths, FileTextChanges uses unix paths. Convert to backslashes. - const unixFilePath = fileTextChanges[0].fileName - const firstChangedFile = /^[a-z]:\//i.test(unixFilePath) ? - unixFilePath.replace(/\//g, '\\') : - unixFilePath - - const configuration = this.projectManager.getConfiguration(firstChangedFile) - configuration.ensureBasicFiles(span) - - const changes: {[uri: string]: TextEdit[]} = {} - for (const change of fileTextChanges) { - const sourceFile = this._getSourceFile(configuration, change.fileName, span) - if (!sourceFile) { - throw new Error(`Expected source file ${change.fileName} to exist in configuration`) + return this.projectManager + .ensureOwnFiles(span) + .concat( + Observable.defer(() => { + // Configuration lookup uses Windows paths, FileTextChanges uses unix paths. Convert to backslashes. + const unixFilePath = fileTextChanges[0].fileName + const firstChangedFile = /^[a-z]:\//i.test(unixFilePath) + ? unixFilePath.replace(/\//g, '\\') + : unixFilePath + + const configuration = this.projectManager.getConfiguration(firstChangedFile) + configuration.ensureBasicFiles(span) + + const changes: { [uri: string]: TextEdit[] } = {} + for (const change of fileTextChanges) { + const sourceFile = this._getSourceFile(configuration, change.fileName, span) + if (!sourceFile) { + throw new Error(`Expected source file ${change.fileName} to exist in configuration`) + } + const uri = path2uri(change.fileName) + changes[uri] = change.textChanges.map(({ span, newText }): TextEdit => ({ + range: { + start: ts.getLineAndCharacterOfPosition(sourceFile, span.start), + end: ts.getLineAndCharacterOfPosition(sourceFile, span.start + span.length), + }, + newText, + })) } - const uri = path2uri(change.fileName) - changes[uri] = change.textChanges.map(({ span, newText }): TextEdit => ({ - range: { - start: ts.getLineAndCharacterOfPosition(sourceFile, span.start), - end: ts.getLineAndCharacterOfPosition(sourceFile, span.start + span.length), - }, - newText, - })) - } - return this.client.workspaceApplyEdit({ edit: { changes }}, span) - })) - .map(() => ({ op: 'add', path: '', value: null }) as Operation) + return this.client.workspaceApplyEdit({ edit: { changes } }, span) + }) + ) + .map(() => ({ op: 'add', path: '', value: null } as Operation)) } /** @@ -1283,41 +1447,51 @@ export class TypeScriptService { public textDocumentRename(params: RenameParams, span = new Span()): Observable { const uri = normalizeUri(params.textDocument.uri) const editUris = new Set() - return this.projectManager.ensureOwnFiles(span) - .concat(Observable.defer(() => { - - const filePath = uri2path(uri) - const configuration = this.projectManager.getParentConfiguration(params.textDocument.uri) - if (!configuration) { - throw new Error(`tsconfig.json not found for ${filePath}`) - } - configuration.ensureAllFiles(span) + return this.projectManager + .ensureOwnFiles(span) + .concat( + Observable.defer(() => { + const filePath = uri2path(uri) + const configuration = this.projectManager.getParentConfiguration(params.textDocument.uri) + if (!configuration) { + throw new Error(`tsconfig.json not found for ${filePath}`) + } + configuration.ensureAllFiles(span) - const sourceFile = this._getSourceFile(configuration, filePath, span) - if (!sourceFile) { - throw new Error(`Expected source file ${filePath} to exist in configuration`) - } + const sourceFile = this._getSourceFile(configuration, filePath, span) + if (!sourceFile) { + throw new Error(`Expected source file ${filePath} to exist in configuration`) + } - const position = ts.getPositionOfLineAndCharacter(sourceFile, params.position.line, params.position.character) + const position = ts.getPositionOfLineAndCharacter( + sourceFile, + params.position.line, + params.position.character + ) - const renameInfo = configuration.getService().getRenameInfo(filePath, position) - if (!renameInfo.canRename) { - throw new Error('This symbol cannot be renamed') - } + const renameInfo = configuration.getService().getRenameInfo(filePath, position) + if (!renameInfo.canRename) { + throw new Error('This symbol cannot be renamed') + } - return Observable.from(configuration.getService().findRenameLocations(filePath, position, false, true)) - .map((location: ts.RenameLocation): [string, TextEdit] => { + return Observable.from( + configuration.getService().findRenameLocations(filePath, position, false, true) + ).map((location: ts.RenameLocation): [string, TextEdit] => { const sourceFile = this._getSourceFile(configuration, location.fileName, span) if (!sourceFile) { throw new Error(`expected source file ${location.fileName} to exist in configuration`) } const editUri = path2uri(location.fileName) const start = ts.getLineAndCharacterOfPosition(sourceFile, location.textSpan.start) - const end = ts.getLineAndCharacterOfPosition(sourceFile, location.textSpan.start + location.textSpan.length) + const end = ts.getLineAndCharacterOfPosition( + sourceFile, + location.textSpan.start + location.textSpan.length + ) const edit: TextEdit = { range: { start, end }, newText: params.newName } return [editUri, edit] }) - })) + }) + ) .map(([uri, edit]): Operation => { // if file has no edit yet, initialize array if (!editUris.has(uri)) { @@ -1387,7 +1561,10 @@ export class TypeScriptService { return } const fileName = uri2path(uri) - const tsDiagnostics = config.getService().getSyntacticDiagnostics(fileName).concat(config.getService().getSemanticDiagnostics(fileName)) + const tsDiagnostics = config + .getService() + .getSyntacticDiagnostics(fileName) + .concat(config.getService().getSemanticDiagnostics(fileName)) const diagnostics = iterate(tsDiagnostics) // TS can report diagnostics without a file and range in some cases // These cannot be represented as LSP Diagnostics since the range and URI is required @@ -1434,7 +1611,11 @@ export class TypeScriptService { * @param fileName file name to fetch source file for or create it * @param span Span for tracing */ - private _getSourceFile(configuration: ProjectConfiguration, fileName: string, span = new Span()): ts.SourceFile | undefined { + private _getSourceFile( + configuration: ProjectConfiguration, + fileName: string, + span = new Span() + ): ts.SourceFile | undefined { let program = configuration.getProgram(span) if (!program) { return undefined @@ -1458,7 +1639,11 @@ export class TypeScriptService { * @param query A text or SymbolDescriptor query * @return Observable of [match score, SymbolInformation] */ - protected _getSymbolsInConfig(config: ProjectConfiguration, query?: string | Partial, childOf = new Span()): Observable<[number, SymbolInformation]> { + protected _getSymbolsInConfig( + config: ProjectConfiguration, + query?: string | Partial, + childOf = new Span() + ): Observable<[number, SymbolInformation]> { return traceObservable('Get symbols in config', childOf, span => { span.addTags({ config: config.configFilePath, query }) config.ensureAllFiles(span) @@ -1471,66 +1656,102 @@ export class TypeScriptService { if (typeof query === 'string') { // Query by text query // Limit the amount of symbols searched for text queries - return Observable.from(config.getService().getNavigateToItems(query, 100, undefined, false)) - // Exclude dependencies and standard library - .filter(item => !isTypeScriptLibrary(item.fileName) && !item.fileName.includes('/node_modules/')) - // Same score for all - .map(item => [1, navigateToItemToSymbolInformation(item, program, this.root)] as [number, SymbolInformation]) + return ( + Observable.from(config.getService().getNavigateToItems(query, 100, undefined, false)) + // Exclude dependencies and standard library + .filter( + item => !isTypeScriptLibrary(item.fileName) && !item.fileName.includes('/node_modules/') + ) + // Same score for all + .map( + item => + [1, navigateToItemToSymbolInformation(item, program, this.root)] as [ + number, + SymbolInformation + ] + ) + ) } else { const queryWithoutPackage = query && omit>(query, 'package') // Require at least 2 properties to match (or all if less provided) const minScore = Math.min(2, getPropertyCount(query)) const minScoreWithoutPackage = Math.min(2, getPropertyCount(queryWithoutPackage)) const service = config.getService() - return Observable.from(program.getSourceFiles()) - // Exclude dependencies and standard library - .filter(sourceFile => !isTypeScriptLibrary(sourceFile.fileName) && !sourceFile.fileName.includes('/node_modules/')) - .mergeMap(sourceFile => { - try { - const tree = service.getNavigationTree(sourceFile.fileName) - const nodes = observableFromIterable(walkNavigationTree(tree)) - .filter(({ tree, parent }) => navigationTreeIsSymbol(tree)) - let matchedNodes: Observable<{ score: number, tree: ts.NavigationTree, parent?: ts.NavigationTree }> - if (!query) { - matchedNodes = nodes - .map(({ tree, parent }) => ({ score: 1, tree, parent })) - } else { - matchedNodes = nodes - // Get a score how good the symbol matches the SymbolDescriptor (ignoring PackageDescriptor) - .map(({ tree, parent }) => { - const symbolDescriptor = navigationTreeToSymbolDescriptor(tree, parent, sourceFile.fileName, this.root) - const score = getMatchingPropertyCount(queryWithoutPackage, symbolDescriptor) - return { score, tree, parent } - }) - // Require the minimum score without the PackageDescriptor name - .filter(({ score }) => score >= minScoreWithoutPackage) - // If SymbolDescriptor matched, get package.json and match PackageDescriptor name - // TODO get and match full PackageDescriptor (version) - .mergeMap(({ score, tree, parent }) => { - if (!query.package || !query.package.name) { - return [{ score, tree, parent }] - } - const uri = path2uri(sourceFile.fileName) - return this.packageManager.getClosestPackageJson(uri, span) - // If PackageDescriptor matches, increase score - .defaultIfEmpty(undefined) - .map(packageJson => { - if (packageJson && packageJson.name === query.package!.name!) { - score++ - } - return { score, tree, parent } - }) - }) - // Require a minimum score to not return thousands of results - .filter(({ score }) => score >= minScore) + return ( + Observable.from(program.getSourceFiles()) + // Exclude dependencies and standard library + .filter( + sourceFile => + !isTypeScriptLibrary(sourceFile.fileName) && + !sourceFile.fileName.includes('/node_modules/') + ) + .mergeMap(sourceFile => { + try { + const tree = service.getNavigationTree(sourceFile.fileName) + const nodes = observableFromIterable( + walkNavigationTree(tree) + ).filter(({ tree, parent }) => navigationTreeIsSymbol(tree)) + let matchedNodes: Observable<{ + score: number + tree: ts.NavigationTree + parent?: ts.NavigationTree + }> + if (!query) { + matchedNodes = nodes.map(({ tree, parent }) => ({ score: 1, tree, parent })) + } else { + matchedNodes = nodes + // Get a score how good the symbol matches the SymbolDescriptor (ignoring PackageDescriptor) + .map(({ tree, parent }) => { + const symbolDescriptor = navigationTreeToSymbolDescriptor( + tree, + parent, + sourceFile.fileName, + this.root + ) + const score = getMatchingPropertyCount( + queryWithoutPackage, + symbolDescriptor + ) + return { score, tree, parent } + }) + // Require the minimum score without the PackageDescriptor name + .filter(({ score }) => score >= minScoreWithoutPackage) + // If SymbolDescriptor matched, get package.json and match PackageDescriptor name + // TODO get and match full PackageDescriptor (version) + .mergeMap(({ score, tree, parent }) => { + if (!query.package || !query.package.name) { + return [{ score, tree, parent }] + } + const uri = path2uri(sourceFile.fileName) + return ( + this.packageManager + .getClosestPackageJson(uri, span) + // If PackageDescriptor matches, increase score + .defaultIfEmpty(undefined) + .map(packageJson => { + if (packageJson && packageJson.name === query.package!.name!) { + score++ + } + return { score, tree, parent } + }) + ) + }) + // Require a minimum score to not return thousands of results + .filter(({ score }) => score >= minScore) + } + return matchedNodes.map( + ({ score, tree, parent }) => + [ + score, + navigationTreeToSymbolInformation(tree, parent, sourceFile, this.root), + ] as [number, SymbolInformation] + ) + } catch (e) { + this.logger.error('Could not get navigation tree for file', sourceFile.fileName) + return [] } - return matchedNodes - .map(({ score, tree, parent }) => [score, navigationTreeToSymbolInformation(tree, parent, sourceFile, this.root)] as [number, SymbolInformation]) - } catch (e) { - this.logger.error('Could not get navigation tree for file', sourceFile.fileName) - return [] - } - }) + }) + ) } }) } diff --git a/src/typings/string-similarity.d.ts b/src/typings/string-similarity.d.ts index ce1e030c5..3f0a50121 100644 --- a/src/typings/string-similarity.d.ts +++ b/src/typings/string-similarity.d.ts @@ -1,4 +1,3 @@ - declare module 'string-similarity' { export function compareTwoStrings(a: string, b: string): number } diff --git a/tsfmt.json b/tsfmt.json deleted file mode 100644 index e2899e24c..000000000 --- a/tsfmt.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "indentSize": 4, - "newLineCharacter": "\n", - "convertTabsToSpaces": true, - "insertSpaceAfterCommaDelimiter": true, - "insertSpaceAfterSemicolonInForStatements": true, - "insertSpaceBeforeAndAfterBinaryOperators": true, - "insertSpaceAfterKeywordsInControlFlowStatements": true, - "insertSpaceAfterFunctionKeywordForAnonymousFunctions": true, - "insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis": false, - "insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets": false, - "insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces": false, - "placeOpenBraceOnNewLineForFunctions": false, - "placeOpenBraceOnNewLineForControlBlocks": false -} From c4fc53ebca84f351d02f513349826b582d54825e Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Mon, 6 Nov 2017 22:18:24 -0800 Subject: [PATCH 02/17] chore: pin prettier to 1.7.4 (#391) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1e1e2d9bc..4a048af6c 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "husky": "^0.14.0", "mocha": "^4.0.0", "nyc": "^11.0.2", - "prettier": "^1.7.4", + "prettier": "1.7.4", "rimraf": "^2.6.1", "semantic-release": "^8.0.0", "sinon": "^4.0.0", From f1542d0d1b98269033059707d399f097c41cc6ca Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Tue, 14 Nov 2017 00:19:27 -0800 Subject: [PATCH 03/17] style: update configs --- .prettierignore | 1 + .vscode/tasks.json | 7 +++++++ package.json | 4 ++-- src/server.ts | 5 +++-- src/typescript-service.ts | 11 ++++++----- tslint.json | 2 -- 6 files changed, 19 insertions(+), 11 deletions(-) create mode 100644 .prettierignore diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..ec6d3cdd7 --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +package.json diff --git a/.vscode/tasks.json b/.vscode/tasks.json index ef2035e3c..6993408a9 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -28,6 +28,13 @@ "problemMatcher": [ "$tslint5" ] + }, + { + "type": "npm", + "script": "tslint", + "problemMatcher": [ + "$tslint5" + ] } ] } diff --git a/package.json b/package.json index 4a048af6c..5c16a4b3b 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "devDependencies": { "@sourcegraph/prettierrc": "^1.1.0", "@sourcegraph/tsconfig": "^1.0.0", - "@sourcegraph/tslint-config": "^7.0.0", + "@sourcegraph/tslint-config": "^8.0.0", "@types/chai": "^4.0.0", "@types/chai-as-promised": "^7.1.0", "@types/glob": "^5.0.30", @@ -76,7 +76,7 @@ "husky": "^0.14.0", "mocha": "^4.0.0", "nyc": "^11.0.2", - "prettier": "1.7.4", + "prettier": "1.8.2", "rimraf": "^2.6.1", "semantic-release": "^8.0.0", "sinon": "^4.0.0", diff --git a/src/server.ts b/src/server.ts index a8dc15781..0bac63dc8 100644 --- a/src/server.ts +++ b/src/server.ts @@ -47,8 +47,9 @@ export function serve( }) cluster.on('exit', (worker, code, signal) => { logger.error( - `Worker ${worker.id} (PID ${worker.process - .pid}) exited from signal ${signal} with code ${code}, restarting` + `Worker ${worker.id} (PID ${worker.process.pid}) exited from signal ${signal} with code ${ + code + }, restarting` ) cluster.fork() }) diff --git a/src/typescript-service.ts b/src/typescript-service.ts index 6dcb7878f..69ccca3a5 100644 --- a/src/typescript-service.ts +++ b/src/typescript-service.ts @@ -1000,8 +1000,9 @@ export class TypeScriptService { // Continue with next node on error // Workaround for https://github.com/Microsoft/TypeScript/issues/15219 this.logger.error( - `workspace/xreferences: Error getting definition for ${source.fileName} at offset ${node.pos + - 1}`, + `workspace/xreferences: Error getting definition for ${ + source.fileName + } at offset ${node.pos + 1}`, err ) span.log({ @@ -1688,9 +1689,9 @@ export class TypeScriptService { .mergeMap(sourceFile => { try { const tree = service.getNavigationTree(sourceFile.fileName) - const nodes = observableFromIterable( - walkNavigationTree(tree) - ).filter(({ tree, parent }) => navigationTreeIsSymbol(tree)) + const nodes = observableFromIterable(walkNavigationTree(tree)).filter( + ({ tree, parent }) => navigationTreeIsSymbol(tree) + ) let matchedNodes: Observable<{ score: number tree: ts.NavigationTree diff --git a/tslint.json b/tslint.json index af675ae43..aafa71f6a 100644 --- a/tslint.json +++ b/tslint.json @@ -1,11 +1,9 @@ { "extends": "@sourcegraph/tslint-config", "rules": { - "rxjs-add": false, "rxjs-no-add": true, "rxjs-no-wholesale": false, - "no-console": [true, "log", "info", "warn", "error", "trace", "time", "timeEnd", "assert", "count", "dir", "table"] } } From c352bb066f20ec90fb39d987e8a677d643d1e0a5 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Tue, 14 Nov 2017 00:24:37 -0800 Subject: [PATCH 04/17] style: disable rxjs-no-patched --- tslint.json | 1 + 1 file changed, 1 insertion(+) diff --git a/tslint.json b/tslint.json index aafa71f6a..3f6894461 100644 --- a/tslint.json +++ b/tslint.json @@ -4,6 +4,7 @@ "rxjs-add": false, "rxjs-no-add": true, "rxjs-no-wholesale": false, + "rxjs-no-patched": false, "no-console": [true, "log", "info", "warn", "error", "trace", "time", "timeEnd", "assert", "count", "dir", "table"] } } From 04331ea70bde93498e01d0442d8e8d025e4cbcd4 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Fri, 17 Nov 2017 11:48:30 -0800 Subject: [PATCH 05/17] chore(npmignore): extend .npmignore --- .npmignore | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/.npmignore b/.npmignore index f71197f35..9809691e3 100644 --- a/.npmignore +++ b/.npmignore @@ -1,9 +1,22 @@ -node_modules/ -src/ +.editorconfig +.gitattributes .idea/ -.vscode/ .nyc_output/ -coverage/ +.prettierignore +.travis.yml +.vscode/ *.iml +*.log +appveyor.yml +CONTRIBUTING.md +coverage.* +coverage/ +DCO +lib/test/ +node_modules/ npm-debug.log +prettier.config.js +src/ +tsconfig.json +tslint.json yarn.lock From 9c7bd9d0f26b6acbf685f73674695c36028ec554 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Wed, 22 Nov 2017 21:56:20 -0800 Subject: [PATCH 06/17] ci(release): use semantic-release v10 --- .travis.yml | 20 ++++++++++++++++---- package.json | 12 ++++++++++-- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 72f4f0d39..d5722ee7c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,8 @@ node_js: - '6' cache: - yarn: true + directories: + - $HOME/.npm script: - npm run lint @@ -13,9 +14,20 @@ script: - npm run cover after_success: - # Upload coverage to codecov - - 'nyc report --reporter=lcov > coverage.lcov && bash <(curl -s https://codecov.io/bash) -f coverage/lcov.info' - - npm run semantic-release + - nyc report --reporter=json + - bash <(curl -s https://codecov.io/bash) + +jobs: + include: + - stage: release + node_js: '8' + script: + - npm run semantic-release + +stages: + - test + - name: release + if: branch = master AND type = push AND fork = false branches: except: diff --git a/package.json b/package.json index 5c16a4b3b..3efe4cb7c 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "prettier": "prettier --list-different --write \"src/**/*.ts\"", "build": "tsc", "watch": "tsc -w", - "semantic-release": "semantic-release pre && npm publish && semantic-release post", + "semantic-release": "semantic-release", "commitmsg": "validate-commit-msg" }, "dependencies": { @@ -57,6 +57,8 @@ "vscode-languageserver-types": "^3.0.3" }, "devDependencies": { + "@semantic-release/github": "^1.0.0", + "@semantic-release/npm": "^1.0.0", "@sourcegraph/prettierrc": "^1.1.0", "@sourcegraph/tsconfig": "^1.0.0", "@sourcegraph/tslint-config": "^8.0.0", @@ -78,7 +80,7 @@ "nyc": "^11.0.2", "prettier": "1.8.2", "rimraf": "^2.6.1", - "semantic-release": "^8.0.0", + "semantic-release": "^10.0.1", "sinon": "^4.0.0", "source-map-support": "^0.5.0", "temp": "^0.8.3", @@ -98,6 +100,12 @@ "lib/test/**/*.js" ] }, + "release": { + "verifyConditions": [ + "@semantic-release/github", + "@semantic-release/npm" + ] + }, "config": { "commitizen": { "path": "./node_modules/cz-conventional-changelog" From be48974a1d4a0a008c4370f569903a81244f59c0 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Wed, 22 Nov 2017 22:02:44 -0800 Subject: [PATCH 07/17] ci(release): add build to release stage --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index d5722ee7c..d2aecfa43 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,6 +22,7 @@ jobs: - stage: release node_js: '8' script: + - npm run build - npm run semantic-release stages: From 8548aadb9baa14c3ce54e209118deaf9e9fdf15e Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Wed, 22 Nov 2017 22:08:59 -0800 Subject: [PATCH 08/17] ci(release): don't upload to codecov in release stage --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index d2aecfa43..8d6f4b556 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,6 +24,7 @@ jobs: script: - npm run build - npm run semantic-release + after_success: skip stages: - test From 31f4323c3f31868891f9de9bc8152e75d620acfc Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Fri, 24 Nov 2017 19:01:24 -0800 Subject: [PATCH 09/17] chore(package): update @semantic-release/github to version 2.0.0 (#405) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3efe4cb7c..7506cd009 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "vscode-languageserver-types": "^3.0.3" }, "devDependencies": { - "@semantic-release/github": "^1.0.0", + "@semantic-release/github": "^2.0.0", "@semantic-release/npm": "^1.0.0", "@sourcegraph/prettierrc": "^1.1.0", "@sourcegraph/tsconfig": "^1.0.0", From e604e3f166b4cc0fb761b4023aaae1f177e2f607 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Fri, 24 Nov 2017 19:31:37 -0800 Subject: [PATCH 10/17] ci(appveyor): improve appveyor config --- appveyor.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/appveyor.yml b/appveyor.yml index 49f310126..1923cbbe6 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,6 +1,10 @@ version: '{build}' +image: Visual Studio 2017 + clone_depth: 1 +skip_branch_with_pr: true +skip_tags: true cache: - '%LOCALAPPDATA%\Yarn' From 8633823f282bb02de27e66b6e6b1fb70fc284faa Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Fri, 24 Nov 2017 20:43:44 -0800 Subject: [PATCH 11/17] chore(package): update @semantic-release/npm to version 2.0.0 (#406) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7506cd009..1f8fb4c2d 100644 --- a/package.json +++ b/package.json @@ -57,8 +57,8 @@ "vscode-languageserver-types": "^3.0.3" }, "devDependencies": { + "@semantic-release/npm": "^2.0.0", "@semantic-release/github": "^2.0.0", - "@semantic-release/npm": "^1.0.0", "@sourcegraph/prettierrc": "^1.1.0", "@sourcegraph/tsconfig": "^1.0.0", "@sourcegraph/tslint-config": "^8.0.0", From 60414623e464e70f1f0f1a731e22126acbbeee4b Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Fri, 24 Nov 2017 20:44:00 -0800 Subject: [PATCH 12/17] chore(package): update @sourcegraph/tslint-config to version 9.0.2 (#404) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1f8fb4c2d..d853decf0 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "@semantic-release/github": "^2.0.0", "@sourcegraph/prettierrc": "^1.1.0", "@sourcegraph/tsconfig": "^1.0.0", - "@sourcegraph/tslint-config": "^8.0.0", + "@sourcegraph/tslint-config": "^9.0.2", "@types/chai": "^4.0.0", "@types/chai-as-promised": "^7.1.0", "@types/glob": "^5.0.30", From 02edc4147b2b3769e20fb8b3d98dc6e2e2801993 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Fri, 24 Nov 2017 20:44:15 -0800 Subject: [PATCH 13/17] chore(package): update @types/sinon to version 4.0.0 (#396) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d853decf0..7bf2cf31e 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "@types/node": "^7.0.32", "@types/object-hash": "^1.1.0", "@types/rimraf": "^2.0.2", - "@types/sinon": "^2.3.5", + "@types/sinon": "^4.0.0", "@types/temp": "^0.8.29", "commitizen": "^2.9.6", "cz-conventional-changelog": "^2.0.0", From 01224dde5e7a6869182b3bedd9fb1453b91703de Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Fri, 24 Nov 2017 20:54:23 -0800 Subject: [PATCH 14/17] chore(package): update @sourcegraph/prettierrc to version 2.0.0 (#398) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7bf2cf31e..ac4d7d08b 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "devDependencies": { "@semantic-release/npm": "^2.0.0", "@semantic-release/github": "^2.0.0", - "@sourcegraph/prettierrc": "^1.1.0", + "@sourcegraph/prettierrc": "^2.0.0", "@sourcegraph/tsconfig": "^1.0.0", "@sourcegraph/tslint-config": "^9.0.2", "@types/chai": "^4.0.0", From d02f2340b22000cfdcdace7c32d1dad71a439063 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Sun, 3 Dec 2017 15:07:12 -0800 Subject: [PATCH 15/17] chore: pin @types/chai to 4.0.6 (#412) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ac4d7d08b..ffad112a4 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "@sourcegraph/prettierrc": "^2.0.0", "@sourcegraph/tsconfig": "^1.0.0", "@sourcegraph/tslint-config": "^9.0.2", - "@types/chai": "^4.0.0", + "@types/chai": "4.0.6", "@types/chai-as-promised": "^7.1.0", "@types/glob": "^5.0.30", "@types/lodash": "^4.14.76", From d1f55da2c71aa734061c8fb6a160e86c6e50f682 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Tue, 5 Dec 2017 01:33:40 -0800 Subject: [PATCH 16/17] chore(package): update prettier to version 1.9.0 (#414) --- package.json | 2 +- src/server.ts | 6 +- src/test/typescript-service-helpers.ts | 247 +++++++++++-------------- 3 files changed, 110 insertions(+), 145 deletions(-) diff --git a/package.json b/package.json index ffad112a4..94ab9a7d4 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "husky": "^0.14.0", "mocha": "^4.0.0", "nyc": "^11.0.2", - "prettier": "1.8.2", + "prettier": "1.9.0", "rimraf": "^2.6.1", "semantic-release": "^10.0.1", "sinon": "^4.0.0", diff --git a/src/server.ts b/src/server.ts index 0bac63dc8..ab3ebed76 100644 --- a/src/server.ts +++ b/src/server.ts @@ -47,9 +47,9 @@ export function serve( }) cluster.on('exit', (worker, code, signal) => { logger.error( - `Worker ${worker.id} (PID ${worker.process.pid}) exited from signal ${signal} with code ${ - code - }, restarting` + `Worker ${worker.id} (PID ${ + worker.process.pid + }) exited from signal ${signal} with code ${code}, restarting` ) cluster.fork() }) diff --git a/src/test/typescript-service-helpers.ts b/src/test/typescript-service-helpers.ts index 30b52b8eb..3bc7bbc4a 100644 --- a/src/test/typescript-service-helpers.ts +++ b/src/test/typescript-service-helpers.ts @@ -612,9 +612,8 @@ export function describeTypeScriptService( afterEach(shutdownService) describe('workspaceSymbol()', function(this: TestContext & ISuiteCallbackContext): void { - it('should find a symbol by SymbolDescriptor query with name and package name', async function( - this: TestContext & ITestCallbackContext - ): Promise { + it('should find a symbol by SymbolDescriptor query with name and package name', async function(this: TestContext & + ITestCallbackContext): Promise { const result: SymbolInformation[] = await this.service .workspaceSymbol({ symbol: { name: 'resolveCallback', package: { name: '@types/resolve' } }, @@ -641,9 +640,8 @@ export function describeTypeScriptService( }, ]) }) - it('should find a symbol by SymbolDescriptor query with name, containerKind and package name', async function( - this: TestContext & ITestCallbackContext - ): Promise { + it('should find a symbol by SymbolDescriptor query with name, containerKind and package name', async function(this: TestContext & + ITestCallbackContext): Promise { const result: SymbolInformation[] = await this.service .workspaceSymbol({ symbol: { @@ -700,9 +698,8 @@ export function describeTypeScriptService( describe('workspaceSymbol()', function(this: TestContext & ISuiteCallbackContext): void { describe('with SymbolDescriptor query', function(this: TestContext & ISuiteCallbackContext): void { - it('should find a symbol by name, kind and package name', async function( - this: TestContext & ITestCallbackContext - ): Promise { + it('should find a symbol by name, kind and package name', async function(this: TestContext & + ITestCallbackContext): Promise { const result: SymbolInformation[] = await this.service .workspaceSymbol({ symbol: { @@ -733,9 +730,8 @@ export function describeTypeScriptService( name: 'a', }) }) - it('should find a symbol by name, kind, package name and ignore package version', async function( - this: TestContext & ITestCallbackContext - ): Promise { + it('should find a symbol by name, kind, package name and ignore package version', async function(this: TestContext & + ITestCallbackContext): Promise { const result: SymbolInformation[] = await this.service .workspaceSymbol({ symbol: { name: 'a', kind: 'class', package: { name: 'mypkg', version: '203940234' } }, @@ -760,9 +756,9 @@ export function describeTypeScriptService( name: 'a', }) }) - it('should find a symbol by name', async function( - this: TestContext & ITestCallbackContext - ): Promise { + it('should find a symbol by name', async function(this: TestContext & ITestCallbackContext): Promise< + void + > { const result: SymbolInformation[] = await this.service .workspaceSymbol({ symbol: { @@ -791,9 +787,8 @@ export function describeTypeScriptService( }, ]) }) - it('should return no result if the PackageDescriptor does not match', async function( - this: TestContext & ITestCallbackContext - ): Promise { + it('should return no result if the PackageDescriptor does not match', async function(this: TestContext & + ITestCallbackContext): Promise { const result: SymbolInformation[] = await this.service .workspaceSymbol({ symbol: { @@ -835,9 +830,8 @@ export function describeTypeScriptService( }, ]) }) - it('should return all symbols for an empty query excluding dependencies', async function( - this: TestContext & ITestCallbackContext - ): Promise { + it('should return all symbols for an empty query excluding dependencies', async function(this: TestContext & + ITestCallbackContext): Promise { const result: SymbolInformation[] = await this.service .workspaceSymbol({ query: '' }) .reduce(applyReducer, null as any) @@ -1007,9 +1001,8 @@ export function describeTypeScriptService( }) describe('workspaceXreferences()', function(this: TestContext & ISuiteCallbackContext): void { - it('should return all references to a method', async function( - this: TestContext & ITestCallbackContext - ): Promise { + it('should return all references to a method', async function(this: TestContext & + ITestCallbackContext): Promise { const result: ReferenceInformation[] = await this.service .workspaceXreferences({ query: { name: 'foo', kind: 'method', containerName: 'a' } }) .reduce(applyReducer, null as any) @@ -1039,9 +1032,8 @@ export function describeTypeScriptService( }, ]) }) - it('should return all references to a method with hinted dependee package name', async function( - this: TestContext & ITestCallbackContext - ): Promise { + it('should return all references to a method with hinted dependee package name', async function(this: TestContext & + ITestCallbackContext): Promise { const result: ReferenceInformation[] = await this.service .workspaceXreferences({ query: { @@ -1080,9 +1072,8 @@ export function describeTypeScriptService( }, ]) }) - it('should return no references to a method if hinted dependee package name was not found', async function( - this: TestContext & ITestCallbackContext - ): Promise { + it('should return no references to a method if hinted dependee package name was not found', async function(this: TestContext & + ITestCallbackContext): Promise { const result = await this.service .workspaceXreferences({ query: { @@ -1098,9 +1089,8 @@ export function describeTypeScriptService( .toPromise() assert.deepEqual(result, []) }) - it('should return all references to a symbol from a dependency', async function( - this: TestContext & ITestCallbackContext - ): Promise { + it('should return all references to a symbol from a dependency', async function(this: TestContext & + ITestCallbackContext): Promise { const result: ReferenceInformation[] = await this.service .workspaceXreferences({ query: { name: 'x' } }) .reduce(applyReducer, null as any) @@ -1130,9 +1120,8 @@ export function describeTypeScriptService( }, ]) }) - it('should return all references to a symbol from a dependency with PackageDescriptor query', async function( - this: TestContext & ITestCallbackContext - ): Promise { + it('should return all references to a symbol from a dependency with PackageDescriptor query', async function(this: TestContext & + ITestCallbackContext): Promise { const result: ReferenceInformation[] = await this.service .workspaceXreferences({ query: { name: 'x', package: { name: 'dep' } } }) .reduce(applyReducer, null as any) @@ -1167,9 +1156,8 @@ export function describeTypeScriptService( }, ]) }) - it('should return all references to all symbols if empty SymbolDescriptor query is passed', async function( - this: TestContext & ITestCallbackContext - ): Promise { + it('should return all references to all symbols if empty SymbolDescriptor query is passed', async function(this: TestContext & + ITestCallbackContext): Promise { const result: ReferenceInformation[] = await this.service .workspaceXreferences({ query: {} }) .reduce(applyReducer, null as any) @@ -1434,9 +1422,9 @@ export function describeTypeScriptService( afterEach(shutdownService) describe('workspaceXdependencies()', function(this: TestContext & ISuiteCallbackContext): void { - it('should account for all dependencies', async function( - this: TestContext & ITestCallbackContext - ): Promise { + it('should account for all dependencies', async function(this: TestContext & ITestCallbackContext): Promise< + void + > { const result: DependencyReference[] = await this.service .workspaceXdependencies() .reduce(applyReducer, null as any) @@ -1536,9 +1524,9 @@ export function describeTypeScriptService( }) }) describe('workspaceXpackages()', function(this: TestContext & ISuiteCallbackContext): void { - it('should accournt for all packages', async function( - this: TestContext & ITestCallbackContext - ): Promise { + it('should accournt for all packages', async function(this: TestContext & ITestCallbackContext): Promise< + void + > { const result: PackageInformation[] = await this.service .workspaceXpackages() .reduce(applyReducer, null as any) @@ -1697,9 +1685,8 @@ export function describeTypeScriptService( afterEach(shutdownService) - it('should handle didChange when configuration is not yet initialized', async function( - this: TestContext & ITestCallbackContext - ): Promise { + it('should handle didChange when configuration is not yet initialized', async function(this: TestContext & + ITestCallbackContext): Promise { const hoverParams = { textDocument: { uri: rootUri + 'a.ts', @@ -1743,9 +1730,8 @@ export function describeTypeScriptService( }) }) - it('should handle didClose when configuration is not yet initialized', async function( - this: TestContext & ITestCallbackContext - ): Promise { + it('should handle didClose when configuration is not yet initialized', async function(this: TestContext & + ITestCallbackContext): Promise { const hoverParams = { textDocument: { uri: rootUri + 'a.ts', @@ -1889,9 +1875,9 @@ export function describeTypeScriptService( afterEach(shutdownService) - it('should publish diagnostics on didOpen', async function( - this: TestContext & ITestCallbackContext - ): Promise { + it('should publish diagnostics on didOpen', async function(this: TestContext & ITestCallbackContext): Promise< + void + > { await this.service.textDocumentDidOpen({ textDocument: { uri: rootUri + 'src/errors.ts', @@ -1916,9 +1902,9 @@ export function describeTypeScriptService( }) }) - it('should publish diagnostics on didChange', async function( - this: TestContext & ITestCallbackContext - ): Promise { + it('should publish diagnostics on didChange', async function(this: TestContext & ITestCallbackContext): Promise< + void + > { await this.service.textDocumentDidOpen({ textDocument: { uri: rootUri + 'src/errors.ts', @@ -1953,9 +1939,8 @@ export function describeTypeScriptService( }) }) - it('should publish empty diagnostics on didChange if error was fixed', async function( - this: TestContext & ITestCallbackContext - ): Promise { + it('should publish empty diagnostics on didChange if error was fixed', async function(this: TestContext & + ITestCallbackContext): Promise { await this.service.textDocumentDidOpen({ textDocument: { uri: rootUri + 'src/errors.ts', @@ -1982,9 +1967,9 @@ export function describeTypeScriptService( }) }) - it('should clear diagnostics on didClose', async function( - this: TestContext & ITestCallbackContext - ): Promise { + it('should clear diagnostics on didClose', async function(this: TestContext & ITestCallbackContext): Promise< + void + > { await this.service.textDocumentDidClose({ textDocument: { uri: rootUri + 'src/errors.ts', @@ -2038,9 +2023,8 @@ export function describeTypeScriptService( afterEach(shutdownService) describe('textDocumentDefinition()', function(this: TestContext & ISuiteCallbackContext): void { - it('should resolve symbol imported with tripe-slash reference', async function( - this: TestContext & ITestCallbackContext - ): Promise { + it('should resolve symbol imported with tripe-slash reference', async function(this: TestContext & + ITestCallbackContext): Promise { const result: Location[] = await this.service .textDocumentDefinition({ textDocument: { @@ -2077,9 +2061,8 @@ export function describeTypeScriptService( }, ]) }) - it('should resolve symbol imported with import statement', async function( - this: TestContext & ITestCallbackContext - ): Promise { + it('should resolve symbol imported with import statement', async function(this: TestContext & + ITestCallbackContext): Promise { const result: Location[] = await this.service .textDocumentDefinition({ textDocument: { @@ -2108,9 +2091,8 @@ export function describeTypeScriptService( }, ]) }) - it('should resolve definition with missing reference', async function( - this: TestContext & ITestCallbackContext - ): Promise { + it('should resolve definition with missing reference', async function(this: TestContext & + ITestCallbackContext): Promise { const result: Location[] = await this.service .textDocumentDefinition({ textDocument: { @@ -2139,9 +2121,9 @@ export function describeTypeScriptService( }, ]) }) - it('should resolve deep definitions', async function( - this: TestContext & ITestCallbackContext - ): Promise { + it('should resolve deep definitions', async function(this: TestContext & ITestCallbackContext): Promise< + void + > { // This test passes only because we expect no response from LSP server // for definition located in file references with depth 3 or more (a -> b -> c -> d (...)) // This test will fail once we'll increase (or remove) depth limit @@ -2198,9 +2180,9 @@ export function describeTypeScriptService( afterEach(shutdownService) describe('textDocumentHover()', function(this: TestContext & ISuiteCallbackContext): void { - it('should load local library file', async function( - this: TestContext & ITestCallbackContext - ): Promise { + it('should load local library file', async function(this: TestContext & ITestCallbackContext): Promise< + void + > { const result: Hover = await this.service .textDocumentHover({ textDocument: { @@ -2259,9 +2241,8 @@ export function describeTypeScriptService( }) }) describe('textDocumentDefinition()', function(this: TestContext & ISuiteCallbackContext): void { - it('should resolve TS libraries to github URL', async function( - this: TestContext & ITestCallbackContext - ): Promise { + it('should resolve TS libraries to github URL', async function(this: TestContext & + ITestCallbackContext): Promise { assert.deepEqual( await this.service .textDocumentDefinition({ @@ -2374,9 +2355,8 @@ export function describeTypeScriptService( afterEach(shutdownService) - it('should provide an empty response when no reference is found', async function( - this: TestContext & ITestCallbackContext - ): Promise { + it('should provide an empty response when no reference is found', async function(this: TestContext & + ITestCallbackContext): Promise { const result = await this.service .textDocumentReferences({ textDocument: { @@ -2393,9 +2373,8 @@ export function describeTypeScriptService( assert.deepEqual(result, []) }) - it('should include the declaration if requested', async function( - this: TestContext & ITestCallbackContext - ): Promise { + it('should include the declaration if requested', async function(this: TestContext & + ITestCallbackContext): Promise { const result = await this.service .textDocumentReferences({ textDocument: { @@ -2426,9 +2405,8 @@ export function describeTypeScriptService( ]) }) - it('should provide a reference within the same file', async function( - this: TestContext & ITestCallbackContext - ): Promise { + it('should provide a reference within the same file', async function(this: TestContext & + ITestCallbackContext): Promise { const result = await this.service .textDocumentReferences({ textDocument: { @@ -2458,9 +2436,8 @@ export function describeTypeScriptService( }, ]) }) - it('should provide two references from imports', async function( - this: TestContext & ITestCallbackContext - ): Promise { + it('should provide two references from imports', async function(this: TestContext & + ITestCallbackContext): Promise { const result = await this.service .textDocumentReferences({ textDocument: { @@ -2549,9 +2526,8 @@ export function describeTypeScriptService( afterEach(shutdownService) - it('should provide a valid empty response when no signature is found', async function( - this: TestContext & ITestCallbackContext - ): Promise { + it('should provide a valid empty response when no signature is found', async function(this: TestContext & + ITestCallbackContext): Promise { const result: SignatureHelp = await this.service .textDocumentSignatureHelp({ textDocument: { @@ -2571,9 +2547,8 @@ export function describeTypeScriptService( }) }) - it('should provide signature help with parameters in the same file', async function( - this: TestContext & ITestCallbackContext - ): Promise { + it('should provide signature help with parameters in the same file', async function(this: TestContext & + ITestCallbackContext): Promise { const result: SignatureHelp = await this.service .textDocumentSignatureHelp({ textDocument: { @@ -2608,9 +2583,8 @@ export function describeTypeScriptService( }) }) - it('should provide signature help from imported symbols', async function( - this: TestContext & ITestCallbackContext - ): Promise { + it('should provide signature help from imported symbols', async function(this: TestContext & + ITestCallbackContext): Promise { const result: SignatureHelp = await this.service .textDocumentSignatureHelp({ textDocument: { @@ -2734,9 +2708,8 @@ export function describeTypeScriptService( ]) }) - it('should resolve completions with snippets', async function( - this: TestContext & ITestCallbackContext - ): Promise { + it('should resolve completions with snippets', async function(this: TestContext & + ITestCallbackContext): Promise { const result: CompletionList = await this.service .textDocumentCompletion({ textDocument: { @@ -2851,9 +2824,9 @@ export function describeTypeScriptService( afterEach(shutdownService) - it('produces completions in the same file', async function( - this: TestContext & ITestCallbackContext - ): Promise { + it('produces completions in the same file', async function(this: TestContext & ITestCallbackContext): Promise< + void + > { const result: CompletionList = await this.service .textDocumentCompletion({ textDocument: { @@ -2911,9 +2884,9 @@ export function describeTypeScriptService( ]) }) - it('resolves completions in the same file', async function( - this: TestContext & ITestCallbackContext - ): Promise { + it('resolves completions in the same file', async function(this: TestContext & ITestCallbackContext): Promise< + void + > { const result: CompletionList = await this.service .textDocumentCompletion({ textDocument: { @@ -2980,9 +2953,8 @@ export function describeTypeScriptService( ]) }) - it('produces completions for imported symbols', async function( - this: TestContext & ITestCallbackContext - ): Promise { + it('produces completions for imported symbols', async function(this: TestContext & + ITestCallbackContext): Promise { const result: CompletionList = await this.service .textDocumentCompletion({ textDocument: { @@ -3011,9 +2983,8 @@ export function describeTypeScriptService( ], }) }) - it('produces completions for referenced symbols', async function( - this: TestContext & ITestCallbackContext - ): Promise { + it('produces completions for referenced symbols', async function(this: TestContext & + ITestCallbackContext): Promise { const result: CompletionList = await this.service .textDocumentCompletion({ textDocument: { @@ -3042,9 +3013,9 @@ export function describeTypeScriptService( ], }) }) - it('produces completions for empty files', async function( - this: TestContext & ITestCallbackContext - ): Promise { + it('produces completions for empty files', async function(this: TestContext & ITestCallbackContext): Promise< + void + > { this.timeout(10000) const result: CompletionList = await this.service .textDocumentCompletion({ @@ -3094,9 +3065,9 @@ export function describeTypeScriptService( afterEach(shutdownService) - it('should error on an invalid symbol', async function( - this: TestContext & ITestCallbackContext - ): Promise { + it('should error on an invalid symbol', async function(this: TestContext & ITestCallbackContext): Promise< + void + > { await Promise.resolve( assert.isRejected( this.service @@ -3116,9 +3087,8 @@ export function describeTypeScriptService( ) ) }) - it('should return a correct WorkspaceEdit to rename a class', async function( - this: TestContext & ITestCallbackContext - ): Promise { + it('should return a correct WorkspaceEdit to rename a class', async function(this: TestContext & + ITestCallbackContext): Promise { const result: WorkspaceEdit = await this.service .textDocumentRename({ textDocument: { @@ -3165,9 +3135,8 @@ export function describeTypeScriptService( }, }) }) - it('should return a correct WorkspaceEdit to rename an imported function', async function( - this: TestContext & ITestCallbackContext - ): Promise { + it('should return a correct WorkspaceEdit to rename an imported function', async function(this: TestContext & + ITestCallbackContext): Promise { const result: WorkspaceEdit = await this.service .textDocumentRename({ textDocument: { @@ -3340,9 +3309,8 @@ export function describeTypeScriptService( afterEach(shutdownService) describe('codeFix', () => { - it('should apply a WorkspaceEdit for the passed FileTextChanges', async function( - this: TestContext & ITestCallbackContext - ): Promise { + it('should apply a WorkspaceEdit for the passed FileTextChanges', async function(this: TestContext & + ITestCallbackContext): Promise { await this.service .workspaceExecuteCommand({ command: 'codeFix', @@ -3405,9 +3373,8 @@ export function describeTypeScriptService( afterEach(shutdownService) - it('should accept files with TypeScript keywords in path', async function( - this: TestContext & ITestCallbackContext - ): Promise { + it('should accept files with TypeScript keywords in path', async function(this: TestContext & + ITestCallbackContext): Promise { const result: Hover = await this.service .textDocumentHover({ textDocument: { @@ -3434,9 +3401,8 @@ export function describeTypeScriptService( contents: [{ language: 'typescript', value: 'function a(): void' }, '**function** _(exported)_'], }) }) - it('should accept files with special characters in path', async function( - this: TestContext & ITestCallbackContext - ): Promise { + it('should accept files with special characters in path', async function(this: TestContext & + ITestCallbackContext): Promise { const result: Hover = await this.service .textDocumentHover({ textDocument: { @@ -3463,9 +3429,8 @@ export function describeTypeScriptService( contents: [{ language: 'typescript', value: 'function b(): void' }, '**function** _(exported)_'], }) }) - it('should handle Windows-style paths in triple slash references', async function( - this: TestContext & ITestCallbackContext - ): Promise { + it('should handle Windows-style paths in triple slash references', async function(this: TestContext & + ITestCallbackContext): Promise { const result = await this.service .textDocumentDefinition({ textDocument: { From 71f103d48955c0ed40db104bfef484c919852d68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dan=20Ros=C3=A9n?= Date: Tue, 5 Dec 2017 11:16:22 +0100 Subject: [PATCH 17/17] fix(hover): correct hover information for type signatures with callbacks (#415) Fixes #413 --- src/test/typescript-service-helpers.ts | 36 ++++++++++++++++++++++++++ src/typescript-service.ts | 2 +- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/test/typescript-service-helpers.ts b/src/test/typescript-service-helpers.ts index 3bc7bbc4a..01813a0fc 100644 --- a/src/test/typescript-service-helpers.ts +++ b/src/test/typescript-service-helpers.ts @@ -124,6 +124,10 @@ export function describeTypeScriptService( [rootUri + 'foo/b.ts', ['/* This is class Foo */', 'export class Foo {}'].join('\n')], [rootUri + 'foo/c.ts', 'import {Foo} from "./b";'], [rootUri + 'd.ts', ['export interface I {', ' target: string;', '}'].join('\n')], + [ + rootUri + 'local_callback.ts', + 'function local(): void { function act(handle: () => void): void { handle() } }', + ], [ rootUri + 'e.ts', [ @@ -322,6 +326,38 @@ export function describeTypeScriptService( contents: [{ language: 'typescript', value: 'const abc: 1' }, '**const**'], }) }) + specify('local function with callback argument', async function( + this: TestContext & ITestCallbackContext + ): Promise { + const result: Hover = await this.service + .textDocumentHover({ + textDocument: { + uri: rootUri + 'local_callback.ts', + }, + position: { + line: 0, + character: 36, + }, + }) + .reduce(applyReducer, null as any) + .toPromise() + assert.deepEqual(result, { + range: { + start: { + line: 0, + character: 34, + }, + end: { + line: 0, + character: 37, + }, + }, + contents: [ + { language: 'typescript', value: 'act(handle: () => void): void' }, + '**local function**', + ], + }) + }) specify('in other file', async function(this: TestContext & ITestCallbackContext): Promise { const result: Hover = await this.service .textDocumentHover({ diff --git a/src/typescript-service.ts b/src/typescript-service.ts index 69ccca3a5..a81bd068a 100644 --- a/src/typescript-service.ts +++ b/src/typescript-service.ts @@ -609,7 +609,7 @@ export class TypeScriptService { } const contents: (MarkedString | string)[] = [] // Add declaration without the kind - const declaration = ts.displayPartsToString(info.displayParts).replace(/^\(.+\)\s+/, '') + const declaration = ts.displayPartsToString(info.displayParts).replace(/^\(.+?\)\s+/, '') contents.push({ language: 'typescript', value: declaration }) // Add kind with modifiers, e.g. "method (private, ststic)", "class (exported)" if (info.kind) {