diff --git a/scripts/buildProtocol.ts b/scripts/buildProtocol.ts index c2ac33c83fc19..899ab700bf372 100644 --- a/scripts/buildProtocol.ts +++ b/scripts/buildProtocol.ts @@ -51,22 +51,25 @@ class DeclarationsWalker { return this.processType((type).typeArguments[0]); } else { - for (const decl of s.getDeclarations()) { - const sourceFile = decl.getSourceFile(); - if (sourceFile === this.protocolFile || path.basename(sourceFile.fileName) === "lib.d.ts") { - return; - } - if (decl.kind === ts.SyntaxKind.EnumDeclaration && !isStringEnum(decl as ts.EnumDeclaration)) { - this.removedTypes.push(type); - return; - } - else { - // splice declaration in final d.ts file - let text = decl.getFullText(); - this.text += `${text}\n`; - // recursively pull all dependencies into result dts file + const declarations = s.getDeclarations(); + if (declarations) { + for (const decl of declarations) { + const sourceFile = decl.getSourceFile(); + if (sourceFile === this.protocolFile || path.basename(sourceFile.fileName) === "lib.d.ts") { + return; + } + if (decl.kind === ts.SyntaxKind.EnumDeclaration && !isStringEnum(decl as ts.EnumDeclaration)) { + this.removedTypes.push(type); + return; + } + else { + // splice declaration in final d.ts file + let text = decl.getFullText(); + this.text += `${text}\n`; + // recursively pull all dependencies into result dts file - this.visitTypeNodes(decl); + this.visitTypeNodes(decl); + } } } } diff --git a/src/compiler/builder.ts b/src/compiler/builder.ts index 192f1e430278e..03e1089a498fa 100644 --- a/src/compiler/builder.ts +++ b/src/compiler/builder.ts @@ -1,23 +1,6 @@ /// namespace ts { - export interface EmitOutput { - outputFiles: OutputFile[]; - emitSkipped: boolean; - } - - export interface EmitOutputDetailed extends EmitOutput { - diagnostics: Diagnostic[]; - sourceMaps: SourceMapData[]; - emittedSourceFiles: SourceFile[]; - } - - export interface OutputFile { - name: string; - writeByteOrderMark: boolean; - text: string; - } - export function getFileEmitOutput(program: Program, sourceFile: SourceFile, emitOnlyDtsFiles: boolean, isDetailed: boolean, cancellationToken?: CancellationToken, customTransformers?: CustomTransformers): EmitOutput | EmitOutputDetailed { const outputFiles: OutputFile[] = []; diff --git a/src/compiler/core.ts b/src/compiler/core.ts index af02cec79c501..8a09c8d403861 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -1829,7 +1829,9 @@ namespace ts { return i < 0 ? path : path.substring(i + 1); } - export function combinePaths(path1: string, path2: string) { + export function combinePaths(path1: Path, path2: string): Path; + export function combinePaths(path1: string, path2: string): string; + export function combinePaths(path1: string, path2: string): string { if (!(path1 && path1.length)) return path2; if (!(path2 && path2.length)) return path1; if (getRootLength(path2) !== 0) return path2; diff --git a/src/compiler/moduleNameResolver.ts b/src/compiler/moduleNameResolver.ts index ac83dd4131195..f8176efa1a94c 100644 --- a/src/compiler/moduleNameResolver.ts +++ b/src/compiler/moduleNameResolver.ts @@ -124,7 +124,11 @@ namespace ts { } } - export function getEffectiveTypeRoots(options: CompilerOptions, host: { directoryExists?: (directoryName: string) => boolean, getCurrentDirectory?: () => string }): string[] | undefined { + export interface GetEffectiveTypeRootsHost { + directoryExists?: (directoryName: string) => boolean; + getCurrentDirectory?: () => string; + } + export function getEffectiveTypeRoots(options: CompilerOptions, host: GetEffectiveTypeRootsHost): string[] | undefined { if (options.typeRoots) { return options.typeRoots; } @@ -985,7 +989,8 @@ namespace ts { return withPackageId(packageId, pathAndExtension); } - function getPackageName(moduleName: string): { packageName: string, rest: string } { + /* @internal */ + export function getPackageName(moduleName: string): { packageName: string, rest: string } { let idx = moduleName.indexOf(directorySeparator); if (moduleName[0] === "@") { idx = moduleName.indexOf(directorySeparator, idx + 1); @@ -1153,4 +1158,67 @@ namespace ts { function toSearchResult(value: T | undefined): SearchResult { return value !== undefined ? { value } : undefined; } + + + + export const enum PackageNameValidationResult { + Ok, + ScopedPackagesNotSupported, + EmptyName, + NameTooLong, + NameStartsWithDot, + NameStartsWithUnderscore, + NameContainsNonURISafeCharacters + } + + const MaxPackageNameLength = 214; + + /** + * Validates package name using rules defined at https://docs.npmjs.com/files/package.json + */ + export function validatePackageName(packageName: string): PackageNameValidationResult { + if (!packageName) { + return PackageNameValidationResult.EmptyName; + } + if (packageName.length > MaxPackageNameLength) { + return PackageNameValidationResult.NameTooLong; + } + if (packageName.charCodeAt(0) === CharacterCodes.dot) { + return PackageNameValidationResult.NameStartsWithDot; + } + if (packageName.charCodeAt(0) === CharacterCodes._) { + return PackageNameValidationResult.NameStartsWithUnderscore; + } + // check if name is scope package like: starts with @ and has one '/' in the middle + // scoped packages are not currently supported + // TODO: when support will be added we'll need to split and check both scope and package name + if (/^@[^/]+\/[^/]+$/.test(packageName)) { + return PackageNameValidationResult.ScopedPackagesNotSupported; + } + if (encodeURIComponent(packageName) !== packageName) { + return PackageNameValidationResult.NameContainsNonURISafeCharacters; + } + return PackageNameValidationResult.Ok; + } + + export function renderPackageNameValidationFailure(result: PackageNameValidationResult, typing: string): string { + switch (result) { + case PackageNameValidationResult.EmptyName: + return `Package name '${typing}' cannot be empty`; + case PackageNameValidationResult.NameTooLong: + return `Package name '${typing}' should be less than ${MaxPackageNameLength} characters`; + case PackageNameValidationResult.NameStartsWithDot: + return `Package name '${typing}' cannot start with '.'`; + case PackageNameValidationResult.NameStartsWithUnderscore: + return `Package name '${typing}' cannot start with '_'`; + case PackageNameValidationResult.ScopedPackagesNotSupported: + return `Package '${typing}' is scoped and currently is not supported`; + case PackageNameValidationResult.NameContainsNonURISafeCharacters: + return `Package name '${typing}' contains non URI safe characters`; + case PackageNameValidationResult.Ok: + throw Debug.fail(); // Shouldn't have called this. + default: + Debug.assertNever(result); + } + } } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 528664f5eeeff..53f4ab8a8958c 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -1,4 +1,21 @@ namespace ts { + export interface EmitOutput { + outputFiles: OutputFile[]; + emitSkipped: boolean; + } + + export interface EmitOutputDetailed extends EmitOutput { + diagnostics: Diagnostic[]; + sourceMaps: SourceMapData[]; + emittedSourceFiles: SourceFile[]; + } + + export interface OutputFile { + name: string; + writeByteOrderMark: boolean; + text: string; + } + /** * Type of objects whose values are all of the same type. * The `in` and `for-in` operators can *not* be safely used, diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index de6d92eda05ed..52f82f7276523 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -953,6 +953,10 @@ namespace FourSlash { return this.getChecker().getSymbolsInScope(node, ts.SymbolFlags.Value | ts.SymbolFlags.Type | ts.SymbolFlags.Namespace); } + public setTypesRegistry(map: ts.MapLike): void { + this.languageServiceAdapterHost.typesRegistry = ts.createMapFromTemplate(map); + } + public verifyTypeOfSymbolAtLocation(range: Range, symbol: ts.Symbol, expected: string): void { const node = this.goToAndGetNode(range); const checker = this.getChecker(); @@ -2750,16 +2754,26 @@ Actual: ${stringify(fullActual)}`); } } - public verifyCodeFixAvailable(negative: boolean) { - const codeFix = this.getCodeFixActions(this.activeFile.fileName); + public verifyCodeFixAvailable(negative: boolean, info: FourSlashInterface.VerifyCodeFixAvailableOptions[] | undefined) { + const codeFixes = this.getCodeFixActions(this.activeFile.fileName); - if (negative && codeFix.length) { - this.raiseError(`verifyCodeFixAvailable failed - expected no fixes but found one.`); + if (negative) { + if (codeFixes.length) { + this.raiseError(`verifyCodeFixAvailable failed - expected no fixes but found one.`); + } + return; } - if (!(negative || codeFix.length)) { + if (!codeFixes.length) { this.raiseError(`verifyCodeFixAvailable failed - expected code fixes but none found.`); } + if (info) { + assert.equal(info.length, codeFixes.length); + ts.zipWith(codeFixes, info, (fix, info) => { + assert.equal(fix.description, info.description); + this.assertObjectsEqual(fix.commands, info.commands); + }); + } } public verifyApplicableRefactorAvailableAtMarker(negative: boolean, markerName: string) { @@ -2803,6 +2817,14 @@ Actual: ${stringify(fullActual)}`); } } + public verifyRefactor({ name, actionName, refactors }: FourSlashInterface.VerifyRefactorOptions) { + const selection = this.getSelection(); + + const actualRefactors = (this.languageService.getApplicableRefactors(this.activeFile.fileName, selection) || ts.emptyArray) + .filter(r => r.name === name && r.actions.some(a => a.name === actionName)); + this.assertObjectsEqual(actualRefactors, refactors); + } + public verifyApplicableRefactorAvailableForRange(negative: boolean) { const ranges = this.getRanges(); if (!(ranges && ranges.length === 1)) { @@ -3577,6 +3599,10 @@ namespace FourSlashInterface { public symbolsInScope(range: FourSlash.Range): ts.Symbol[] { return this.state.symbolsInScope(range); } + + public setTypesRegistry(map: ts.MapLike): void { + this.state.setTypesRegistry(map); + } } export class GoTo { @@ -3752,8 +3778,8 @@ namespace FourSlashInterface { this.state.verifyCodeFix(options); } - public codeFixAvailable() { - this.state.verifyCodeFixAvailable(this.negative); + public codeFixAvailable(options?: VerifyCodeFixAvailableOptions[]) { + this.state.verifyCodeFixAvailable(this.negative, options); } public applicableRefactorAvailableAtMarker(markerName: string) { @@ -3764,6 +3790,10 @@ namespace FourSlashInterface { this.state.verifyApplicableRefactorAvailableForRange(this.negative); } + public refactor(options: VerifyRefactorOptions) { + this.state.verifyRefactor(options); + } + public refactorAvailable(name: string, actionName?: string) { this.state.verifyRefactorAvailable(this.negative, name, actionName); } @@ -4404,4 +4434,15 @@ namespace FourSlashInterface { errorCode?: number; index?: number; } + + export interface VerifyCodeFixAvailableOptions { + description: string; + commands?: ts.CodeActionCommand[]; + } + + export interface VerifyRefactorOptions { + name: string; + actionName: string; + refactors: ts.ApplicableRefactorInfo[]; + } } diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index ad79c96d833f8..c8d6d3fc00264 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -123,6 +123,7 @@ namespace Harness.LanguageService { } export class LanguageServiceAdapterHost { + public typesRegistry: ts.Map | undefined; protected virtualFileSystem: Utils.VirtualFileSystem = new Utils.VirtualFileSystem(virtualFileSystemRoot, /*useCaseSensitiveFilenames*/false); constructor(protected cancellationToken = DefaultHostCancellationToken.Instance, @@ -182,6 +183,14 @@ namespace Harness.LanguageService { /// Native adapter class NativeLanguageServiceHost extends LanguageServiceAdapterHost implements ts.LanguageServiceHost { + tryGetRegistry(): ts.Map | undefined { + if (this.typesRegistry === undefined) { + ts.Debug.fail("fourslash test should set types registry."); + } + return this.typesRegistry; + } + installPackage = ts.notImplemented; + getCompilationSettings() { return this.settings; } getCancellationToken() { return this.cancellationToken; } getDirectories(path: string): string[] { @@ -493,6 +502,7 @@ namespace Harness.LanguageService { getCodeFixesAtPosition(): ts.CodeAction[] { throw new Error("Not supported on the shim."); } + applyCodeActionCommand = ts.notImplemented; getCodeFixDiagnostics(): ts.Diagnostic[] { throw new Error("Not supported on the shim."); } diff --git a/src/harness/unittests/compileOnSave.ts b/src/harness/unittests/compileOnSave.ts index fdec5b192ee83..7be6ab5b323ec 100644 --- a/src/harness/unittests/compileOnSave.ts +++ b/src/harness/unittests/compileOnSave.ts @@ -12,7 +12,7 @@ namespace ts.projectSystem { describe("CompileOnSave affected list", () => { function sendAffectedFileRequestAndCheckResult(session: server.Session, request: server.protocol.Request, expectedFileList: { projectFileName: string, files: FileOrFolder[] }[]) { - const response: server.protocol.CompileOnSaveAffectedFileListSingleProject[] = session.executeCommand(request).response; + const response = session.executeCommand(request).response as server.protocol.CompileOnSaveAffectedFileListSingleProject[]; const actualResult = response.sort((list1, list2) => compareStrings(list1.projectFileName, list2.projectFileName)); expectedFileList = expectedFileList.sort((list1, list2) => compareStrings(list1.projectFileName, list2.projectFileName)); diff --git a/src/harness/unittests/extractTestHelpers.ts b/src/harness/unittests/extractTestHelpers.ts index 1b51cdadc2785..49c2c1d327704 100644 --- a/src/harness/unittests/extractTestHelpers.ts +++ b/src/harness/unittests/extractTestHelpers.ts @@ -97,6 +97,14 @@ namespace ts { return rulesProvider; } + const notImplementedHost: LanguageServiceHost = { + getCompilationSettings: notImplemented, + getScriptFileNames: notImplemented, + getScriptVersion: notImplemented, + getScriptSnapshot: notImplemented, + getDefaultLibFileName: notImplemented, + }; + export function testExtractSymbol(caption: string, text: string, baselineFolder: string, description: DiagnosticMessage) { const t = extractTest(text); const selectionRange = t.ranges.get("selection"); @@ -125,6 +133,7 @@ namespace ts { file: sourceFile, startPosition: selectionRange.start, endPosition: selectionRange.end, + host: notImplementedHost, rulesProvider: getRuleProvider() }; const rangeToExtract = refactor.extractSymbol.getRangeToExtract(sourceFile, createTextSpanFromBounds(selectionRange.start, selectionRange.end)); @@ -188,6 +197,7 @@ namespace ts { file: sourceFile, startPosition: selectionRange.start, endPosition: selectionRange.end, + host: notImplementedHost, rulesProvider: getRuleProvider() }; const rangeToExtract = refactor.extractSymbol.getRangeToExtract(sourceFile, createTextSpanFromBounds(selectionRange.start, selectionRange.end)); diff --git a/src/harness/unittests/languageService.ts b/src/harness/unittests/languageService.ts index fd0a95c167fae..5d383a314f91a 100644 --- a/src/harness/unittests/languageService.ts +++ b/src/harness/unittests/languageService.ts @@ -21,6 +21,8 @@ export function Component(x: Config): any;` // to write an alias to a module's default export was referrenced across files and had no default export it("should be able to create a language service which can respond to deinition requests without throwing", () => { const languageService = ts.createLanguageService({ + tryGetTypesRegistry: notImplemented, + installPackage: notImplemented, getCompilationSettings() { return {}; }, diff --git a/src/harness/unittests/projectErrors.ts b/src/harness/unittests/projectErrors.ts index d72168383c170..dae465a3ef333 100644 --- a/src/harness/unittests/projectErrors.ts +++ b/src/harness/unittests/projectErrors.ts @@ -57,7 +57,7 @@ namespace ts.projectSystem { }); checkNumberOfProjects(projectService, { externalProjects: 1 }); - const diags = session.executeCommand(compilerOptionsRequest).response; + const diags = session.executeCommand(compilerOptionsRequest).response as server.protocol.DiagnosticWithLinePosition[]; // only file1 exists - expect error checkDiagnosticsWithLinePos(diags, ["File '/a/b/applib.ts' not found."]); } @@ -65,7 +65,7 @@ namespace ts.projectSystem { { // only file2 exists - expect error checkNumberOfProjects(projectService, { externalProjects: 1 }); - const diags = session.executeCommand(compilerOptionsRequest).response; + const diags = session.executeCommand(compilerOptionsRequest).response as server.protocol.DiagnosticWithLinePosition[]; checkDiagnosticsWithLinePos(diags, ["File '/a/b/app.ts' not found."]); } @@ -73,7 +73,7 @@ namespace ts.projectSystem { { // both files exist - expect no errors checkNumberOfProjects(projectService, { externalProjects: 1 }); - const diags = session.executeCommand(compilerOptionsRequest).response; + const diags = session.executeCommand(compilerOptionsRequest).response as server.protocol.DiagnosticWithLinePosition[]; checkDiagnosticsWithLinePos(diags, []); } }); @@ -103,13 +103,13 @@ namespace ts.projectSystem { seq: 2, arguments: { projectFileName: project.getProjectName() } }; - let diags = session.executeCommand(compilerOptionsRequest).response; + let diags = session.executeCommand(compilerOptionsRequest).response as server.protocol.DiagnosticWithLinePosition[]; checkDiagnosticsWithLinePos(diags, ["File '/a/b/applib.ts' not found."]); host.reloadFS([file1, file2, config, libFile]); checkNumberOfProjects(projectService, { configuredProjects: 1 }); - diags = session.executeCommand(compilerOptionsRequest).response; + diags = session.executeCommand(compilerOptionsRequest).response as server.protocol.DiagnosticWithLinePosition[]; checkDiagnosticsWithLinePos(diags, []); }); diff --git a/src/harness/unittests/session.ts b/src/harness/unittests/session.ts index 3b5efc2d6defb..d4f15acde1bbd 100644 --- a/src/harness/unittests/session.ts +++ b/src/harness/unittests/session.ts @@ -315,7 +315,7 @@ namespace ts.server { item: false }; const command = "newhandle"; - const result = { + const result: ts.server.HandlerResponse = { response: respBody, responseRequired: true }; @@ -332,7 +332,7 @@ namespace ts.server { const respBody = { item: false }; - const resp = { + const resp: ts.server.HandlerResponse = { response: respBody, responseRequired: true }; @@ -372,7 +372,7 @@ namespace ts.server { }; const command = "test"; - session.output(body, command); + session.output(body, command, /*reqSeq*/ 0, /*success*/ true); expect(lastSent).to.deep.equal({ seq: 0, @@ -475,7 +475,7 @@ namespace ts.server { }; const command = "test"; - session.output(body, command); + session.output(body, command, /*reqSeq*/ 0, /*success*/ true); expect(session.lastSent).to.deep.equal({ seq: 0, @@ -542,14 +542,14 @@ namespace ts.server { handleRequest(msg: protocol.Request) { let response: protocol.Response; try { - ({ response } = this.executeCommand(msg)); + response = this.executeCommand(msg).response as protocol.Response; } catch (e) { this.output(undefined, msg.command, msg.seq, e.toString()); return; } if (response) { - this.output(response, msg.command, msg.seq); + this.output(response, msg.command, msg.seq, /*success*/ true); } } diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 716c36d1e5d2a..b71cdaf4edd7d 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -70,6 +70,9 @@ namespace ts.projectSystem { protected postExecActions: PostExecAction[] = []; + tryGetTypesRegistry = notImplemented; + installPackage = notImplemented; + executePendingCommands() { const actionsToRun = this.postExecActions; this.postExecActions = []; @@ -761,7 +764,7 @@ namespace ts.projectSystem { ); // Two errors: CommonFile2 not found and cannot find name y - let diags: server.protocol.Diagnostic[] = session.executeCommand(getErrRequest).response; + let diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; verifyDiagnostics(diags, [ { diagnosticMessage: Diagnostics.Cannot_find_name_0, errorTextArguments: ["y"] }, { diagnosticMessage: Diagnostics.File_0_not_found, errorTextArguments: [commonFile2.path] } @@ -773,7 +776,7 @@ namespace ts.projectSystem { assert.strictEqual(projectService.inferredProjects[0], project, "Inferred project should be same"); checkProjectRootFiles(project, [file1.path]); checkProjectActualFiles(project, [file1.path, libFile.path, commonFile2.path]); - diags = session.executeCommand(getErrRequest).response; + diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; verifyNoDiagnostics(diags); }); @@ -2603,11 +2606,11 @@ namespace ts.projectSystem { // Try to find some interface type defined in lib.d.ts const libTypeNavToRequest = makeSessionRequest(CommandNames.Navto, { searchValue: "Document", file: file1.path, projectFileName: configFile.path }); - const items: protocol.NavtoItem[] = session.executeCommand(libTypeNavToRequest).response; + const items = session.executeCommand(libTypeNavToRequest).response as protocol.NavtoItem[]; assert.isFalse(containsNavToItem(items, "Document", "interface"), `Found lib.d.ts symbol in JavaScript project nav to request result.`); const localFunctionNavToRequst = makeSessionRequest(CommandNames.Navto, { searchValue: "foo", file: file1.path, projectFileName: configFile.path }); - const items2: protocol.NavtoItem[] = session.executeCommand(localFunctionNavToRequst).response; + const items2 = session.executeCommand(localFunctionNavToRequst).response as protocol.NavtoItem[]; assert.isTrue(containsNavToItem(items2, "foo", "function"), `Cannot find function symbol "foo".`); }); }); @@ -3062,7 +3065,7 @@ namespace ts.projectSystem { server.CommandNames.SemanticDiagnosticsSync, { file: file1.path } ); - let diags: server.protocol.Diagnostic[] = session.executeCommand(getErrRequest).response; + let diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; verifyNoDiagnostics(diags); const moduleFileOldPath = moduleFile.path; @@ -3070,7 +3073,7 @@ namespace ts.projectSystem { moduleFile.path = moduleFileNewPath; host.reloadFS([moduleFile, file1]); host.runQueuedTimeoutCallbacks(); - diags = session.executeCommand(getErrRequest).response; + diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; verifyDiagnostics(diags, [ { diagnosticMessage: Diagnostics.Cannot_find_module_0, errorTextArguments: ["./moduleFile"] } ]); @@ -3088,7 +3091,7 @@ namespace ts.projectSystem { session.executeCommand(changeRequest); host.runQueuedTimeoutCallbacks(); - diags = session.executeCommand(getErrRequest).response; + diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; verifyNoDiagnostics(diags); }); @@ -3113,7 +3116,7 @@ namespace ts.projectSystem { server.CommandNames.SemanticDiagnosticsSync, { file: file1.path } ); - let diags: server.protocol.Diagnostic[] = session.executeCommand(getErrRequest).response; + let diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; verifyNoDiagnostics(diags); const moduleFileOldPath = moduleFile.path; @@ -3121,7 +3124,7 @@ namespace ts.projectSystem { moduleFile.path = moduleFileNewPath; host.reloadFS([moduleFile, file1, configFile]); host.runQueuedTimeoutCallbacks(); - diags = session.executeCommand(getErrRequest).response; + diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; verifyDiagnostics(diags, [ { diagnosticMessage: Diagnostics.Cannot_find_module_0, errorTextArguments: ["./moduleFile"] } ]); @@ -3129,7 +3132,7 @@ namespace ts.projectSystem { moduleFile.path = moduleFileOldPath; host.reloadFS([moduleFile, file1, configFile]); host.runQueuedTimeoutCallbacks(); - diags = session.executeCommand(getErrRequest).response; + diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; verifyNoDiagnostics(diags); }); @@ -3196,7 +3199,7 @@ namespace ts.projectSystem { server.CommandNames.SemanticDiagnosticsSync, { file: file1.path } ); - let diags: server.protocol.Diagnostic[] = session.executeCommand(getErrRequest).response; + let diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; verifyDiagnostics(diags, [ { diagnosticMessage: Diagnostics.Cannot_find_module_0, errorTextArguments: ["./moduleFile"] } ]); @@ -3212,7 +3215,7 @@ namespace ts.projectSystem { session.executeCommand(changeRequest); // Recheck - diags = session.executeCommand(getErrRequest).response; + diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; verifyNoDiagnostics(diags); }); }); @@ -3805,7 +3808,7 @@ namespace ts.projectSystem { command: server.CommandNames.CompilerOptionsDiagnosticsFull, seq: 2, arguments: { projectFileName: projectName } - }).response; + }).response as ReadonlyArray; assert.isTrue(diags.length === 0); session.executeCommand({ @@ -3819,7 +3822,7 @@ namespace ts.projectSystem { command: server.CommandNames.CompilerOptionsDiagnosticsFull, seq: 4, arguments: { projectFileName: projectName } - }).response; + }).response as ReadonlyArray; assert.isTrue(diagsAfterUpdate.length === 0); }); @@ -3846,7 +3849,7 @@ namespace ts.projectSystem { command: server.CommandNames.CompilerOptionsDiagnosticsFull, seq: 2, arguments: { projectFileName } - }).response; + }).response as ReadonlyArray; assert.isTrue(diags.length === 0); session.executeCommand({ @@ -3864,7 +3867,7 @@ namespace ts.projectSystem { command: server.CommandNames.CompilerOptionsDiagnosticsFull, seq: 4, arguments: { projectFileName } - }).response; + }).response as ReadonlyArray; assert.isTrue(diagsAfterUpdate.length === 0); }); }); @@ -4345,7 +4348,7 @@ namespace ts.projectSystem { command: server.CommandNames.SemanticDiagnosticsSync, seq: 2, arguments: { file: configFile.path, projectFileName: projectName, includeLinePosition: true } - }).response; + }).response as ReadonlyArray; assert.isTrue(diags.length === 2); configFile.content = configFileContentWithoutCommentLine; @@ -4356,7 +4359,7 @@ namespace ts.projectSystem { command: server.CommandNames.SemanticDiagnosticsSync, seq: 2, arguments: { file: configFile.path, projectFileName: projectName, includeLinePosition: true } - }).response; + }).response as ReadonlyArray; assert.isTrue(diagsAfterEdit.length === 2); verifyDiagnostic(diags[0], diagsAfterEdit[0]); @@ -4748,7 +4751,7 @@ namespace ts.projectSystem { line: undefined, offset: undefined }); - const { response } = session.executeCommand(getDefinitionRequest); + const response = session.executeCommand(getDefinitionRequest).response as server.protocol.FileSpan[]; assert.equal(response[0].file, moduleFile.path, "Should go to definition of vessel: response: " + JSON.stringify(response)); callsTrackingHost.verifyNoHostCalls(); diff --git a/src/harness/unittests/typingsInstaller.ts b/src/harness/unittests/typingsInstaller.ts index e644f8730107b..3914283653530 100644 --- a/src/harness/unittests/typingsInstaller.ts +++ b/src/harness/unittests/typingsInstaller.ts @@ -266,7 +266,7 @@ namespace ts.projectSystem { }; const host = createServerHost([file1]); let enqueueIsCalled = false; - const installer = new (class extends Installer { + const installer: Installer = new (class extends Installer { constructor() { super(host, { typesRegistry: createTypesRegistry("jquery") }); } @@ -983,21 +983,21 @@ namespace ts.projectSystem { for (let i = 0; i < 8; i++) { packageName += packageName; } - assert.equal(TI.validatePackageName(packageName), TI.PackageNameValidationResult.NameTooLong); + assert.equal(validatePackageName(packageName), PackageNameValidationResult.NameTooLong); }); it("name cannot start with dot", () => { - assert.equal(TI.validatePackageName(".foo"), TI.PackageNameValidationResult.NameStartsWithDot); + assert.equal(validatePackageName(".foo"), PackageNameValidationResult.NameStartsWithDot); }); it("name cannot start with underscore", () => { - assert.equal(TI.validatePackageName("_foo"), TI.PackageNameValidationResult.NameStartsWithUnderscore); + assert.equal(validatePackageName("_foo"), PackageNameValidationResult.NameStartsWithUnderscore); }); it("scoped packages not supported", () => { - assert.equal(TI.validatePackageName("@scope/bar"), TI.PackageNameValidationResult.ScopedPackagesNotSupported); + assert.equal(validatePackageName("@scope/bar"), PackageNameValidationResult.ScopedPackagesNotSupported); }); it("non URI safe characters are not supported", () => { - assert.equal(TI.validatePackageName(" scope "), TI.PackageNameValidationResult.NameContainsNonURISafeCharacters); - assert.equal(TI.validatePackageName("; say ‘Hello from TypeScript!’ #"), TI.PackageNameValidationResult.NameContainsNonURISafeCharacters); - assert.equal(TI.validatePackageName("a/b/c"), TI.PackageNameValidationResult.NameContainsNonURISafeCharacters); + assert.equal(validatePackageName(" scope "), PackageNameValidationResult.NameContainsNonURISafeCharacters); + assert.equal(validatePackageName("; say ‘Hello from TypeScript!’ #"), PackageNameValidationResult.NameContainsNonURISafeCharacters); + assert.equal(validatePackageName("a/b/c"), PackageNameValidationResult.NameContainsNonURISafeCharacters); }); }); @@ -1250,7 +1250,7 @@ namespace ts.projectSystem { const host = createServerHost([f1, packageFile]); let beginEvent: server.BeginInstallTypes; let endEvent: server.EndInstallTypes; - const installer = new (class extends Installer { + const installer: Installer = new (class extends Installer { constructor() { super(host, { globalTypingsCacheLocation: cachePath, typesRegistry: createTypesRegistry("commander") }); } diff --git a/src/server/client.ts b/src/server/client.ts index d08d1e13d2e66..ad4c29ab324a0 100644 --- a/src/server/client.ts +++ b/src/server/client.ts @@ -14,7 +14,7 @@ namespace ts.server { } /* @internal */ - export function extractMessage(message: string) { + export function extractMessage(message: string): string { // Read the content length const contentLengthPrefix = "Content-Length: "; const lines = message.split(/\r?\n/); @@ -540,6 +540,16 @@ namespace ts.server { return response.body.map(entry => this.convertCodeActions(entry, file)); } + applyCodeActionCommand(file: string, command: CodeActionCommand): PromiseLike { + const args: protocol.ApplyCodeActionCommandRequestArgs = { file, command }; + + const request = this.processRequest(CommandNames.ApplyCodeActionCommand, args); + // TODO: how can we possibly get it synchronously here? But is SessionClient test-only? + const response = this.processResponse(request); + + return PromiseImpl.resolved({ successMessage: response.message }); + } + private createFileLocationOrRangeRequestArgs(positionOrRange: number | TextRange, fileName: string): protocol.FileLocationOrRangeRequestArgs { return typeof positionOrRange === "number" ? this.createFileLocationRequestArgs(fileName, positionOrRange) diff --git a/src/server/project.ts b/src/server/project.ts index 7f7c3132136f2..2779d2a1c168c 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -242,6 +242,19 @@ namespace ts.server { this.markAsDirty(); } + /*@internal*/ + protected abstract getProjectRootPath(): Path | undefined; + + tryGetTypesRegistry(): Map | undefined { + return this.typingsCache.tryGetTypesRegistry(); + } + installPackage(options: InstallPackageOptions): PromiseLike { + return this.typingsCache.installPackage({ ...options, projectRootPath: this.getProjectRootPath() }); + } + private get typingsCache(): TypingsCache { + return this.projectService.typingsCache; + } + getCompilationSettings() { return this.compilerOptions; } @@ -1104,6 +1117,11 @@ namespace ts.server { exclude: [] }; } + + /*@internal*/ + protected getProjectRootPath(): Path | undefined { + return this.projectRootPath as Path; + } } /** @@ -1364,6 +1382,11 @@ namespace ts.server { this.projectErrors.push(getErrorForNoInputFiles(this.configFileSpecs, this.getConfigFilePath())); } } + + /* @internal */ + protected getProjectRootPath(): Path | undefined { + return this.canonicalConfigFilePath as string as Path; + } } /** @@ -1380,7 +1403,7 @@ namespace ts.server { compilerOptions: CompilerOptions, languageServiceEnabled: boolean, public compileOnSaveEnabled: boolean, - projectFilePath?: string) { + private readonly projectFilePath?: string) { super(externalProjectName, ProjectKind.External, projectService, @@ -1424,5 +1447,10 @@ namespace ts.server { } this.typeAcquisition = newTypeAcquisition; } + + /* @internal */ + protected getProjectRootPath(): Path | undefined { + return this.projectFilePath as Path; + } } } diff --git a/src/server/protocol.ts b/src/server/protocol.ts index 3d07392bbe69f..3add00080329d 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -94,6 +94,7 @@ namespace ts.server.protocol { BreakpointStatement = "breakpointStatement", CompilerOptionsForInferredProjects = "compilerOptionsForInferredProjects", GetCodeFixes = "getCodeFixes", + ApplyCodeActionCommand = "applyCodeActionCommand", /* @internal */ GetCodeFixesFull = "getCodeFixes-full", GetSupportedCodeFixes = "getSupportedCodeFixes", @@ -125,6 +126,8 @@ namespace ts.server.protocol { * Client-initiated request message */ export interface Request extends Message { + type: "request"; + /** * The command to execute */ @@ -147,6 +150,8 @@ namespace ts.server.protocol { * Server-initiated event message */ export interface Event extends Message { + type: "event"; + /** * Name of event */ @@ -162,6 +167,8 @@ namespace ts.server.protocol { * Response by server to client request message. */ export interface Response extends Message { + type: "response"; + /** * Sequence number of the request message. */ @@ -178,7 +185,8 @@ namespace ts.server.protocol { command: string; /** - * Contains error message if success === false. + * If success === false, this should always be provided. + * Otherwise, may (or may not) contain a success message. */ message?: string; @@ -520,6 +528,14 @@ namespace ts.server.protocol { arguments: CodeFixRequestArgs; } + export interface ApplyCodeActionCommandRequest extends Request { + command: CommandTypes.ApplyCodeActionCommand; + arguments: ApplyCodeActionCommandRequestArgs; + } + + // All we need is the `success` and `message` fields of Response. + export interface ApplyCodeActionCommandResponse extends Response {} + export interface FileRangeRequestArgs extends FileRequestArgs { /** * The line number for the request (1-based). @@ -564,6 +580,10 @@ namespace ts.server.protocol { errorCodes?: number[]; } + export interface ApplyCodeActionCommandRequestArgs extends FileRequestArgs { + command: {}; + } + /** * Response for GetCodeFixes request. */ @@ -1541,6 +1561,7 @@ namespace ts.server.protocol { description: string; /** Text changes to apply to each file as part of the code action */ changes: FileCodeEdits[]; + commands?: {}[]; } /** diff --git a/src/server/server.ts b/src/server/server.ts index f24251ee6ac90..d304ec2fff49e 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -250,6 +250,7 @@ namespace ts.server { private activeRequestCount = 0; private requestQueue: QueuedOperation[] = []; private requestMap = createMap(); // Maps operation ID to newest requestQueue entry with that ID + private typesRegistryCache: Map | undefined; // This number is essentially arbitrary. Processing more than one typings request // at a time makes sense, but having too many in the pipe results in a hang @@ -258,7 +259,7 @@ namespace ts.server { // buffer, but we have yet to find a way to retrieve that value. private static readonly maxActiveRequestCount = 10; private static readonly requestDelayMillis = 100; - + private packageInstalledPromise: PromiseImpl; constructor( private readonly telemetryEnabled: boolean, @@ -278,6 +279,23 @@ namespace ts.server { } } + tryGetTypesRegistry(): Map | undefined { + if (this.typesRegistryCache) { + return this.typesRegistryCache; + } + + this.send({ kind: "typesRegistry" }); + return undefined; + } + + installPackage(options: InstallPackageOptionsWithProjectRootPath): PromiseLike { + const rq: InstallPackageRequest = { kind: "installPackage", ...options }; + this.send(rq); + Debug.assert(this.packageInstalledPromise === undefined); + this.packageInstalledPromise = PromiseImpl.deferred(); + return this.packageInstalledPromise; + } + private reportInstallerProcessId() { if (this.installerPidReported) { return; @@ -343,7 +361,11 @@ namespace ts.server { } onProjectClosed(p: Project): void { - this.installer.send({ projectName: p.getProjectName(), kind: "closeProject" }); + this.send({ projectName: p.getProjectName(), kind: "closeProject" }); + } + + private send(rq: TypingInstallerRequestUnion): void { + this.installer.send(rq); } enqueueInstallTypingsRequest(project: Project, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray): void { @@ -359,7 +381,7 @@ namespace ts.server { if (this.logger.hasLevel(LogLevel.verbose)) { this.logger.info(`Sending request: ${JSON.stringify(request)}`); } - this.installer.send(request); + this.send(request); }; const queuedRequest: QueuedOperation = { operationId, operation }; @@ -375,12 +397,26 @@ namespace ts.server { } } - private handleMessage(response: SetTypings | InvalidateCachedTypings | BeginInstallTypes | EndInstallTypes | InitializationFailedResponse) { + private handleMessage(response: TypesRegistryResponse | PackageInstalledResponse | SetTypings | InvalidateCachedTypings | BeginInstallTypes | EndInstallTypes | InitializationFailedResponse) { if (this.logger.hasLevel(LogLevel.verbose)) { this.logger.info(`Received response: ${JSON.stringify(response)}`); } switch (response.kind) { + case EventTypesRegistry: + this.typesRegistryCache = ts.createMapFromTemplate(response.typesRegistry); + break; + case EventPackageInstalled: { + const { success, message } = response; + if (success) { + this.packageInstalledPromise.resolve({ successMessage: message }); + } + else { + this.packageInstalledPromise.reject(message); + } + this.packageInstalledPromise = undefined; + break; + } case EventInitializationFailed: { if (!this.eventSender) { diff --git a/src/server/session.ts b/src/server/session.ts index 800d09ff6c283..3972327ba69a0 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -411,19 +411,22 @@ namespace ts.server { this.send(ev); } - public output(info: any, cmdName: string, reqSeq = 0, errorMsg?: string) { + public output(info: {} | undefined, cmdName: string, reqSeq: number, success: boolean, message?: string) { const res: protocol.Response = { seq: 0, type: "response", command: cmdName, request_seq: reqSeq, - success: !errorMsg, + success, }; - if (!errorMsg) { + if (success) { res.body = info; } else { - res.message = errorMsg; + Debug.assert(info === undefined); + } + if (message) { + res.message = message; } this.send(res); } @@ -1296,7 +1299,7 @@ namespace ts.server { this.changeSeq++; // make sure no changes happen before this one is finished if (project.reloadScript(file, tempFileName)) { - this.output(undefined, CommandNames.Reload, reqSeq); + this.output(undefined, CommandNames.Reload, reqSeq, /*success*/ true); } } @@ -1534,6 +1537,15 @@ namespace ts.server { } } + private applyCodeActionCommand(commandName: string, requestSeq: number, args: protocol.ApplyCodeActionCommandRequestArgs): void { + const { file, project } = this.getFileAndProject(args); + const output = (success: boolean, message: string) => this.output({}, commandName, requestSeq, success, message); + const command = args.command as CodeActionCommand; // They should be sending back the command we sent them. + project.getLanguageService().applyCodeActionCommand(file, command).then( + ({ successMessage }) => { output(/*success*/ true, successMessage); }, + error => { output(/*success*/ false, error); }); + } + private getStartAndEndPosition(args: protocol.FileRangeRequestArgs, scriptInfo: ScriptInfo) { let startPosition: number = undefined, endPosition: number = undefined; if (args.startPosition !== undefined) { @@ -1556,14 +1568,12 @@ namespace ts.server { return { startPosition, endPosition }; } - private mapCodeAction(codeAction: CodeAction, scriptInfo: ScriptInfo): protocol.CodeAction { - return { - description: codeAction.description, - changes: codeAction.changes.map(change => ({ - fileName: change.fileName, - textChanges: change.textChanges.map(textChange => this.convertTextChangeToCodeEdit(textChange, scriptInfo)) - })) - }; + private mapCodeAction({ description, changes: unmappedChanges, commands }: CodeAction, scriptInfo: ScriptInfo): protocol.CodeAction { + const changes = unmappedChanges.map(change => ({ + fileName: change.fileName, + textChanges: change.textChanges.map(textChange => this.convertTextChangeToCodeEdit(textChange, scriptInfo)) + })); + return { description, changes, commands }; } private mapTextChangesToCodeEdits(project: Project, textChanges: FileTextChanges): protocol.FileCodeEdits { @@ -1649,15 +1659,15 @@ namespace ts.server { exit() { } - private notRequired() { + private notRequired(): HandlerResponse { return { responseRequired: false }; } - private requiredResponse(response: any) { + private requiredResponse(response: {}): HandlerResponse { return { response, responseRequired: true }; } - private handlers = createMapFromTemplate<(request: protocol.Request) => { response?: any, responseRequired?: boolean }>({ + private handlers = createMapFromTemplate<(request: protocol.Request) => HandlerResponse>({ [CommandNames.OpenExternalProject]: (request: protocol.OpenExternalProjectRequest) => { this.projectService.openExternalProject(request.arguments, /*suppressRefreshOfInferredProjects*/ false); // TODO: report errors @@ -1835,7 +1845,7 @@ namespace ts.server { }, [CommandNames.Configure]: (request: protocol.ConfigureRequest) => { this.projectService.setHostConfiguration(request.arguments); - this.output(undefined, CommandNames.Configure, request.seq); + this.output(undefined, CommandNames.Configure, request.seq, /*success*/ true); return this.notRequired(); }, [CommandNames.Reload]: (request: protocol.ReloadRequest) => { @@ -1902,6 +1912,10 @@ namespace ts.server { [CommandNames.GetCodeFixesFull]: (request: protocol.CodeFixRequest) => { return this.requiredResponse(this.getCodeFixes(request.arguments, /*simplifiedResult*/ false)); }, + [CommandNames.ApplyCodeActionCommand]: (request: protocol.ApplyCodeActionCommandRequest) => { + this.applyCodeActionCommand(request.command, request.seq, request.arguments); + return this.notRequired(); // Response will come asynchronously. + }, [CommandNames.GetSupportedCodeFixes]: () => { return this.requiredResponse(this.getSupportedCodeFixes()); }, @@ -1916,7 +1930,7 @@ namespace ts.server { } }); - public addProtocolHandler(command: string, handler: (request: protocol.Request) => { response?: any, responseRequired: boolean }) { + public addProtocolHandler(command: string, handler: (request: protocol.Request) => HandlerResponse) { if (this.handlers.has(command)) { throw new Error(`Protocol handler already exists for command "${command}"`); } @@ -1945,14 +1959,14 @@ namespace ts.server { } } - public executeCommand(request: protocol.Request): { response?: any, responseRequired?: boolean } { + public executeCommand(request: protocol.Request): HandlerResponse { const handler = this.handlers.get(request.command); if (handler) { return this.executeWithRequestId(request.seq, () => handler(request)); } else { this.logger.msg(`Unrecognized JSON command: ${JSON.stringify(request)}`, Msg.Err); - this.output(undefined, CommandNames.Unknown, request.seq, `Unrecognized JSON command: ${request.command}`); + this.output(undefined, CommandNames.Unknown, request.seq, /*success*/ false, `Unrecognized JSON command: ${request.command}`); return { responseRequired: false }; } } @@ -1983,16 +1997,16 @@ namespace ts.server { } if (response) { - this.output(response, request.command, request.seq); + this.output(response, request.command, request.seq, /*success*/ true); } else if (responseRequired) { - this.output(undefined, request.command, request.seq, "No content available."); + this.output(undefined, request.command, request.seq, /*success*/ false, "No content available."); } } catch (err) { if (err instanceof OperationCanceledException) { // Handle cancellation exceptions - this.output({ canceled: true }, request.command, request.seq); + this.output({ canceled: true }, request.command, request.seq, /*success*/ true); return; } this.logError(err, message); @@ -2000,8 +2014,14 @@ namespace ts.server { undefined, request ? request.command : CommandNames.Unknown, request ? request.seq : 0, + /*success*/ false, "Error processing request. " + (err).message + "\n" + (err).stack); } } } + + export interface HandlerResponse { + response?: {}; + responseRequired?: boolean; + } } diff --git a/src/server/shared.ts b/src/server/shared.ts index 66f739a5b974a..a8a122c3327ee 100644 --- a/src/server/shared.ts +++ b/src/server/shared.ts @@ -3,6 +3,8 @@ namespace ts.server { export const ActionSet: ActionSet = "action::set"; export const ActionInvalidate: ActionInvalidate = "action::invalidate"; + export const EventTypesRegistry: EventTypesRegistry = "event::typesRegistry"; + export const EventPackageInstalled: EventPackageInstalled = "event::packageInstalled"; export const EventBeginInstallTypes: EventBeginInstallTypes = "event::beginInstallTypes"; export const EventEndInstallTypes: EventEndInstallTypes = "event::endInstallTypes"; export const EventInitializationFailed: EventInitializationFailed = "event::initializationFailed"; diff --git a/src/server/types.ts b/src/server/types.ts index 4fc4356a4a97f..b846b9cac0bf0 100644 --- a/src/server/types.ts +++ b/src/server/types.ts @@ -1,6 +1,7 @@ /// /// /// +/// declare namespace ts.server { export interface CompressedData { @@ -28,12 +29,14 @@ declare namespace ts.server { " __sortedArrayBrand": any; } - export interface TypingInstallerRequest { + export interface TypingInstallerRequestWithProjectName { readonly projectName: string; - readonly kind: "discover" | "closeProject"; } - export interface DiscoverTypings extends TypingInstallerRequest { + /* @internal */ + export type TypingInstallerRequestUnion = DiscoverTypings | CloseProject | TypesRegistryRequest | InstallPackageRequest; + + export interface DiscoverTypings extends TypingInstallerRequestWithProjectName { readonly fileNames: string[]; readonly projectRootPath: Path; readonly compilerOptions: CompilerOptions; @@ -43,18 +46,44 @@ declare namespace ts.server { readonly kind: "discover"; } - export interface CloseProject extends TypingInstallerRequest { + export interface CloseProject extends TypingInstallerRequestWithProjectName { readonly kind: "closeProject"; } + export interface TypesRegistryRequest { + readonly kind: "typesRegistry"; + } + + export interface InstallPackageRequest extends InstallPackageOptions { + readonly kind: "installPackage"; + readonly projectRootPath: Path; + } + export type ActionSet = "action::set"; export type ActionInvalidate = "action::invalidate"; + export type EventTypesRegistry = "event::typesRegistry"; + export type EventPackageInstalled = "event::packageInstalled"; export type EventBeginInstallTypes = "event::beginInstallTypes"; export type EventEndInstallTypes = "event::endInstallTypes"; export type EventInitializationFailed = "event::initializationFailed"; export interface TypingInstallerResponse { - readonly kind: ActionSet | ActionInvalidate | EventBeginInstallTypes | EventEndInstallTypes | EventInitializationFailed; + readonly kind: ActionSet | ActionInvalidate | EventTypesRegistry | EventPackageInstalled | EventBeginInstallTypes | EventEndInstallTypes | EventInitializationFailed; + } + /* @internal */ + export type TypingInstallerResponseUnion = SetTypings | InvalidateCachedTypings | TypesRegistryResponse | PackageInstalledResponse | InstallTypes | InitializationFailedResponse; + + /* @internal */ + export interface TypesRegistryResponse extends TypingInstallerResponse { + readonly kind: EventTypesRegistry; + readonly typesRegistry: MapLike; + } + + /* @internal */ + export interface PackageInstalledResponse extends TypingInstallerResponse { + readonly kind: EventPackageInstalled; + readonly success: boolean; + readonly message: string; } export interface InitializationFailedResponse extends TypingInstallerResponse { diff --git a/src/server/typingsCache.ts b/src/server/typingsCache.ts index 207824616a980..81a83b21e35bb 100644 --- a/src/server/typingsCache.ts +++ b/src/server/typingsCache.ts @@ -1,7 +1,13 @@ /// namespace ts.server { + export interface InstallPackageOptionsWithProjectRootPath extends InstallPackageOptions { + projectRootPath: Path; + } + export interface ITypingsInstaller { + tryGetTypesRegistry(): Map | undefined; + installPackage(options: InstallPackageOptionsWithProjectRootPath): PromiseLike; enqueueInstallTypingsRequest(p: Project, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray): void; attach(projectService: ProjectService): void; onProjectClosed(p: Project): void; @@ -9,6 +15,9 @@ namespace ts.server { } export const nullTypingsInstaller: ITypingsInstaller = { + tryGetTypesRegistry: () => undefined, + // Should never be called because we never provide a types registry. + installPackage: notImplemented, enqueueInstallTypingsRequest: noop, attach: noop, onProjectClosed: noop, @@ -77,6 +86,14 @@ namespace ts.server { constructor(private readonly installer: ITypingsInstaller) { } + tryGetTypesRegistry(): Map | undefined { + return this.installer.tryGetTypesRegistry(); + } + + installPackage(options: InstallPackageOptionsWithProjectRootPath): PromiseLike { + return this.installer.installPackage(options); + } + getTypingsForProject(project: Project, unresolvedImports: SortedReadonlyArray, forceRefresh: boolean): SortedReadonlyArray { const typeAcquisition = project.getTypeAcquisition(); diff --git a/src/server/typingsInstaller/nodeTypingsInstaller.ts b/src/server/typingsInstaller/nodeTypingsInstaller.ts index 98478c2d5fc39..a21655d62c970 100644 --- a/src/server/typingsInstaller/nodeTypingsInstaller.ts +++ b/src/server/typingsInstaller/nodeTypingsInstaller.ts @@ -53,7 +53,7 @@ namespace ts.server.typingsInstaller { } try { const content = JSON.parse(host.readFile(typesRegistryFilePath)); - return createMapFromTemplate(content.entries); + return createMapFromTemplate(content.entries); } catch (e) { if (log.isEnabled()) { @@ -79,7 +79,7 @@ namespace ts.server.typingsInstaller { private readonly npmPath: string; readonly typesRegistry: Map; - private delayedInitializationError: InitializationFailedResponse; + private delayedInitializationError: InitializationFailedResponse | undefined; constructor(globalTypingsCacheLocation: string, typingSafeListLocation: string, typesMapLocation: string, npmLocation: string | undefined, throttleLimit: number, log: Log) { super( @@ -127,7 +127,7 @@ namespace ts.server.typingsInstaller { } listen() { - process.on("message", (req: DiscoverTypings | CloseProject) => { + process.on("message", (req: TypingInstallerRequestUnion) => { if (this.delayedInitializationError) { // report initializationFailed error this.sendResponse(this.delayedInitializationError); @@ -139,11 +139,39 @@ namespace ts.server.typingsInstaller { break; case "closeProject": this.closeProject(req); + break; + case "typesRegistry": { + const typesRegistry: { [key: string]: void } = {}; + this.typesRegistry.forEach((value, key) => { + typesRegistry[key] = value; + }); + const response: TypesRegistryResponse = { kind: EventTypesRegistry, typesRegistry }; + this.sendResponse(response); + break; + } + case "installPackage": { + const { fileName, packageName, projectRootPath } = req; + const cwd = getDirectoryOfPackageJson(fileName, this.installTypingHost) || projectRootPath; + if (cwd) { + this.installWorker(-1, [packageName], cwd, success => { + const message = success ? `Package ${packageName} installed.` : `There was an error installing ${packageName}.`; + const response: PackageInstalledResponse = { kind: EventPackageInstalled, success, message }; + this.sendResponse(response); + }); + } + else { + const response: PackageInstalledResponse = { kind: EventPackageInstalled, success: false, message: "Could not determine a project root path." }; + this.sendResponse(response); + } + break; + } + default: + Debug.assertNever(req); } }); } - protected sendResponse(response: SetTypings | InvalidateCachedTypings | BeginInstallTypes | EndInstallTypes | InitializationFailedResponse) { + protected sendResponse(response: TypingInstallerResponseUnion) { if (this.log.isEnabled()) { this.log.writeLine(`Sending response: ${JSON.stringify(response)}`); } @@ -153,11 +181,11 @@ namespace ts.server.typingsInstaller { } } - protected installWorker(requestId: number, args: string[], cwd: string, onRequestCompleted: RequestCompletedAction): void { + protected installWorker(requestId: number, packageNames: string[], cwd: string, onRequestCompleted: RequestCompletedAction): void { if (this.log.isEnabled()) { - this.log.writeLine(`#${requestId} with arguments'${JSON.stringify(args)}'.`); + this.log.writeLine(`#${requestId} with arguments'${JSON.stringify(packageNames)}'.`); } - const command = `${this.npmPath} install --ignore-scripts ${args.join(" ")} --save-dev --user-agent="typesInstaller/${version}"`; + const command = `${this.npmPath} install --ignore-scripts ${packageNames.join(" ")} --save-dev --user-agent="typesInstaller/${version}"`; const start = Date.now(); const hasError = this.execSyncAndLog(command, { cwd }); if (this.log.isEnabled()) { @@ -186,6 +214,14 @@ namespace ts.server.typingsInstaller { } } + function getDirectoryOfPackageJson(fileName: string, host: InstallTypingHost): string | undefined { + return forEachAncestorDirectory(getDirectoryPath(fileName), directory => { + if (host.fileExists(combinePaths(directory, "package.json"))) { + return directory; + } + }); + } + const logFilePath = findArgument(server.Arguments.LogFile); const globalTypingsCacheLocation = findArgument(server.Arguments.GlobalCacheLocation); const typingSafeListLocation = findArgument(server.Arguments.TypingSafeListLocation); diff --git a/src/server/typingsInstaller/typingsInstaller.ts b/src/server/typingsInstaller/typingsInstaller.ts index 3eae0755747b7..bb39201e77ea4 100644 --- a/src/server/typingsInstaller/typingsInstaller.ts +++ b/src/server/typingsInstaller/typingsInstaller.ts @@ -32,50 +32,11 @@ namespace ts.server.typingsInstaller { } } - export enum PackageNameValidationResult { - Ok, - ScopedPackagesNotSupported, - EmptyName, - NameTooLong, - NameStartsWithDot, - NameStartsWithUnderscore, - NameContainsNonURISafeCharacters - } - - - export const MaxPackageNameLength = 214; - /** - * Validates package name using rules defined at https://docs.npmjs.com/files/package.json - */ - export function validatePackageName(packageName: string): PackageNameValidationResult { - if (!packageName) { - return PackageNameValidationResult.EmptyName; - } - if (packageName.length > MaxPackageNameLength) { - return PackageNameValidationResult.NameTooLong; - } - if (packageName.charCodeAt(0) === CharacterCodes.dot) { - return PackageNameValidationResult.NameStartsWithDot; - } - if (packageName.charCodeAt(0) === CharacterCodes._) { - return PackageNameValidationResult.NameStartsWithUnderscore; - } - // check if name is scope package like: starts with @ and has one '/' in the middle - // scoped packages are not currently supported - // TODO: when support will be added we'll need to split and check both scope and package name - if (/^@[^/]+\/[^/]+$/.test(packageName)) { - return PackageNameValidationResult.ScopedPackagesNotSupported; - } - if (encodeURIComponent(packageName) !== packageName) { - return PackageNameValidationResult.NameContainsNonURISafeCharacters; - } - return PackageNameValidationResult.Ok; - } export type RequestCompletedAction = (success: boolean) => void; interface PendingRequest { requestId: number; - args: string[]; + packageNames: string[]; cwd: string; onRequestCompleted: RequestCompletedAction; } @@ -270,26 +231,7 @@ namespace ts.server.typingsInstaller { // add typing name to missing set so we won't process it again this.missingTypingsSet.set(typing, true); if (this.log.isEnabled()) { - switch (validationResult) { - case PackageNameValidationResult.EmptyName: - this.log.writeLine(`Package name '${typing}' cannot be empty`); - break; - case PackageNameValidationResult.NameTooLong: - this.log.writeLine(`Package name '${typing}' should be less than ${MaxPackageNameLength} characters`); - break; - case PackageNameValidationResult.NameStartsWithDot: - this.log.writeLine(`Package name '${typing}' cannot start with '.'`); - break; - case PackageNameValidationResult.NameStartsWithUnderscore: - this.log.writeLine(`Package name '${typing}' cannot start with '_'`); - break; - case PackageNameValidationResult.ScopedPackagesNotSupported: - this.log.writeLine(`Package '${typing}' is scoped and currently is not supported`); - break; - case PackageNameValidationResult.NameContainsNonURISafeCharacters: - this.log.writeLine(`Package name '${typing}' contains non URI safe characters`); - break; - } + this.log.writeLine(renderPackageNameValidationFailure(validationResult, typing)); } } } @@ -430,8 +372,8 @@ namespace ts.server.typingsInstaller { }; } - private installTypingsAsync(requestId: number, args: string[], cwd: string, onRequestCompleted: RequestCompletedAction): void { - this.pendingRunRequests.unshift({ requestId, args, cwd, onRequestCompleted }); + private installTypingsAsync(requestId: number, packageNames: string[], cwd: string, onRequestCompleted: RequestCompletedAction): void { + this.pendingRunRequests.unshift({ requestId, packageNames, cwd, onRequestCompleted }); this.executeWithThrottling(); } @@ -439,7 +381,7 @@ namespace ts.server.typingsInstaller { while (this.inFlightRequestCount < this.throttleLimit && this.pendingRunRequests.length) { this.inFlightRequestCount++; const request = this.pendingRunRequests.pop(); - this.installWorker(request.requestId, request.args, request.cwd, ok => { + this.installWorker(request.requestId, request.packageNames, request.cwd, ok => { this.inFlightRequestCount--; request.onRequestCompleted(ok); this.executeWithThrottling(); @@ -447,7 +389,7 @@ namespace ts.server.typingsInstaller { } } - protected abstract installWorker(requestId: number, args: string[], cwd: string, onRequestCompleted: RequestCompletedAction): void; + protected abstract installWorker(requestId: number, packageNames: string[], cwd: string, onRequestCompleted: RequestCompletedAction): void; protected abstract sendResponse(response: SetTypings | InvalidateCachedTypings | BeginInstallTypes | EndInstallTypes): void; } diff --git a/src/services/codefixes/fixCannotFindModule.ts b/src/services/codefixes/fixCannotFindModule.ts new file mode 100644 index 0000000000000..afd4a85efe8cb --- /dev/null +++ b/src/services/codefixes/fixCannotFindModule.ts @@ -0,0 +1,42 @@ +/* @internal */ +namespace ts.codefix { + registerCodeFix({ + errorCodes: [ + Diagnostics.Cannot_find_module_0.code, + Diagnostics.Could_not_find_a_declaration_file_for_module_0_1_implicitly_has_an_any_type.code, + ], + getCodeActions: context => { + const { sourceFile, span: { start } } = context; + const token = getTokenAtPosition(sourceFile, start, /*includeJsDocComment*/ false); + if (!isStringLiteral(token)) { + throw Debug.fail(); // These errors should only happen on the module name. + } + + const action = tryGetCodeActionForInstallPackageTypes(context.host, token.text); + return action && [action]; + }, + }); + + export function tryGetCodeActionForInstallPackageTypes(host: LanguageServiceHost, moduleName: string): CodeAction | undefined { + const { packageName } = getPackageName(moduleName); + + // We want to avoid looking this up in the registry as that is expensive. So first check that it's actually an NPM package. + const validationResult = validatePackageName(packageName); + if (validationResult !== PackageNameValidationResult.Ok) { + return undefined; + } + + const registry = host.tryGetTypesRegistry(); + if (!registry || !registry.has(packageName)) { + // If !registry, registry not available yet, can't do anything. + return undefined; + } + + const typesPackageName = `@types/${packageName}`; + return { + description: `Install '${typesPackageName}'`, + changes: [], + commands: [{ type: "install package", packageName: typesPackageName }], + }; + } +} diff --git a/src/services/codefixes/fixes.ts b/src/services/codefixes/fixes.ts index b024dfae7cd82..ac0a37bdeb0ef 100644 --- a/src/services/codefixes/fixes.ts +++ b/src/services/codefixes/fixes.ts @@ -3,6 +3,7 @@ /// /// /// +/// /// /// /// diff --git a/src/services/refactorProvider.ts b/src/services/refactorProvider.ts index e956a4121c791..ea08fbeedb190 100644 --- a/src/services/refactorProvider.ts +++ b/src/services/refactorProvider.ts @@ -20,6 +20,7 @@ namespace ts { endPosition?: number; program: Program; newLineCharacter: string; + host: LanguageServiceHost; rulesProvider?: formatting.RulesProvider; cancellationToken?: CancellationToken; } diff --git a/src/services/refactors/installTypesForPackage.ts b/src/services/refactors/installTypesForPackage.ts new file mode 100644 index 0000000000000..ded4a1d47afaf --- /dev/null +++ b/src/services/refactors/installTypesForPackage.ts @@ -0,0 +1,63 @@ +/* @internal */ +namespace ts.refactor.installTypesForPackage { + const actionName = "install"; + + const installTypesForPackage: Refactor = { + name: "Install missing types package", + description: "Install missing types package", + getEditsForAction, + getAvailableActions, + }; + + registerRefactor(installTypesForPackage); + + function getAvailableActions(context: RefactorContext): ApplicableRefactorInfo[] | undefined { + if (context.program.getCompilerOptions().noImplicitAny) { + // Then it will be available via `fixCannotFindModule`. + return undefined; + } + + const action = getAction(context); + return action && [ + { + name: installTypesForPackage.name, + description: installTypesForPackage.description, + actions: [ + { + description: action.description, + name: actionName, + }, + ], + }, + ]; + } + + function getEditsForAction(context: RefactorContext, _actionName: string): RefactorEditInfo | undefined { + Debug.assertEqual(actionName, _actionName); + const action = getAction(context)!; // Should be defined if we said there was an action available. + return { + edits: [], + renameFilename: undefined, + renameLocation: undefined, + commands: action.commands, + }; + } + + function getAction(context: RefactorContext): CodeAction | undefined { + const { file, startPosition } = context; + const node = getTokenAtPosition(file, startPosition, /*includeJsDocComment*/ false); + if (isStringLiteral(node) && isModuleIdentifier(node) && getResolvedModule(file, node.text) === undefined) { + return codefix.tryGetCodeActionForInstallPackageTypes(context.host, node.text); + } + } + + function isModuleIdentifier(node: StringLiteral): boolean { + switch (node.parent.kind) { + case SyntaxKind.ImportDeclaration: + case SyntaxKind.ExternalModuleReference: + return true; + default: + return false; + } + } +} \ No newline at end of file diff --git a/src/services/refactors/refactors.ts b/src/services/refactors/refactors.ts index 680b7f8b02fbe..218180f174f6b 100644 --- a/src/services/refactors/refactors.ts +++ b/src/services/refactors/refactors.ts @@ -1,2 +1,3 @@ /// /// +/// diff --git a/src/services/services.ts b/src/services/services.ts index 6bdc96d8b4d65..8dee40bb626fc 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1761,6 +1761,17 @@ namespace ts { }); } + function applyCodeActionCommand(fileName: Path, action: CodeActionCommand): PromiseLike { + fileName = toPath(fileName, currentDirectory, getCanonicalFileName); + switch (action.type) { + case "install package": + return host.installPackage({ fileName, packageName: action.packageName }); + default: + Debug.fail(); + // TODO: Debug.assertNever(action); will only work if there is more than one type. + } + } + function getDocCommentTemplateAtPosition(fileName: string, position: number): TextInsertion { return JsDoc.getDocCommentTemplateAtPosition(getNewLineOrDefaultFromHost(host), syntaxTreeCache.getCurrentSourceFile(fileName), position); } @@ -1970,8 +1981,9 @@ namespace ts { endPosition, program: getProgram(), newLineCharacter: formatOptions ? formatOptions.newLineCharacter : host.getNewLine(), + host, rulesProvider: getRuleProvider(formatOptions), - cancellationToken + cancellationToken, }; } @@ -2033,6 +2045,7 @@ namespace ts { isValidBraceCompletionAtPosition, getSpanOfEnclosingComment, getCodeFixesAtPosition, + applyCodeActionCommand, getEmitOutput, getNonBoundSourceFile, getSourceFile, diff --git a/src/services/shims.ts b/src/services/shims.ts index 9d4baccc3c4e0..311a99ba272c2 100644 --- a/src/services/shims.ts +++ b/src/services/shims.ts @@ -347,6 +347,16 @@ namespace ts { } } + public tryGetTypesRegistry(): Map | undefined { + throw new Error("TODO"); + } + public installPackage(_options: InstallPackageOptions): PromiseLike { + throw new Error("TODO"); + } + public getTsconfigLocation(): Path | undefined { + throw new Error("TODO"); + } + public log(s: string): void { if (this.loggingEnabled) { this.shimHost.log(s); diff --git a/src/services/types.ts b/src/services/types.ts index e853eb7b96cad..4fcb58bb0b155 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -145,10 +145,15 @@ namespace ts { isCancellationRequested(): boolean; } + export interface InstallPackageOptions { + fileName: Path; + packageName: string; + } + // // Public interface of the host of a language service instance. // - export interface LanguageServiceHost { + export interface LanguageServiceHost extends GetEffectiveTypeRootsHost { getCompilationSettings(): CompilerOptions; getNewLine?(): string; getProjectVersion?(): string; @@ -158,7 +163,6 @@ namespace ts { getScriptSnapshot(fileName: string): IScriptSnapshot | undefined; getLocalizedDiagnosticMessages?(): any; getCancellationToken?(): HostCancellationToken; - getCurrentDirectory(): string; getDefaultLibFileName(options: CompilerOptions): string; log?(s: string): void; trace?(s: string): void; @@ -187,7 +191,6 @@ namespace ts { resolveTypeReferenceDirectives?(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[]; /* @internal */ hasInvalidatedResolution?: HasInvalidatedResolution; /* @internal */ hasChangedAutomaticTypeDirectiveNames?: boolean; - directoryExists?(directoryName: string): boolean; /* * getDirectories is also required for full import and type reference completions. Without it defined, certain @@ -199,6 +202,9 @@ namespace ts { * Gets a set of custom transformers to use during emit. */ getCustomTransformers?(): CustomTransformers | undefined; + + tryGetTypesRegistry?(): Map | undefined; + installPackage?(options: InstallPackageOptions): PromiseLike; } // @@ -275,6 +281,7 @@ namespace ts { getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): TextSpan; getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: number[], formatOptions: FormatCodeSettings): CodeAction[]; + applyCodeActionCommand(fileName: string, action: CodeActionCommand): PromiseLike; getApplicableRefactors(fileName: string, positionOrRaneg: number | TextRange): ApplicableRefactorInfo[]; getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string): RefactorEditInfo | undefined; @@ -294,6 +301,10 @@ namespace ts { dispose(): void; } + export interface ApplyCodeActionCommandResult { + successMessage: string; + } + export interface Classifications { spans: number[]; endOfLineState: EndOfLineState; @@ -366,6 +377,20 @@ namespace ts { description: string; /** Text changes to apply to each file as part of the code action */ changes: FileTextChanges[]; + /** + * If the user accepts the code fix, the editor should send the action back in a `applyAction` request. + * This allows the language service to have side effects (e.g. installing dependencies) upon a code fix. + */ + commands?: CodeActionCommand[]; + } + + // Publicly, this type is just `{}`. Internally it is a union of all the actions we use. + // See `commands?: {}[]` in protocol.ts + export type CodeActionCommand = InstallPackageAction; + + export interface InstallPackageAction { + /* @internal */ type: "install package"; + /* @internal */ packageName: string; } /** @@ -419,6 +444,7 @@ namespace ts { edits: FileTextChanges[]; renameFilename: string | undefined; renameLocation: number | undefined; + commands?: CodeActionCommand[]; } export interface TextInsertion { diff --git a/src/services/utilities.ts b/src/services/utilities.ts index a8dea4ddd0e03..46b19fd11de14 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -1086,6 +1086,98 @@ namespace ts { return !seen[id] && (seen[id] = true); }; } + + + const enum PromiseState { Unresolved, Success, Failure } + /** + * Cheap PromiseLike implementation. + */ + export class PromiseImpl implements PromiseLike { + private constructor( + private state: PromiseState, + private result: T | undefined, + private error: {} | undefined, + // Only supports one callback + private callback: { + onfulfilled?: (value: T) => void, + onrejected?: (value: {}) => void, + } | undefined) {} + + static deferred(): PromiseImpl { + return new PromiseImpl(PromiseState.Unresolved, undefined, undefined, undefined); + } + + static resolved(value: T): PromiseImpl { + return new PromiseImpl(PromiseState.Success, value, undefined, undefined); + } + + resolve(value: T): void { + Debug.assert(this.state === PromiseState.Unresolved); + this.state = PromiseState.Success; + this.result = value; + if (this.callback) { + this.callback.onfulfilled(value); + this.callback = undefined; + } + } + + reject(value: {}): void { + this.state = PromiseState.Failure; + this.error = value; + if (this.callback) { + this.callback.onrejected(value); + this.callback = undefined; + } + } + + then( + onfulfilled?: ((value: T) => TResult1 | PromiseLike) | undefined | null, + onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null, + ): PromiseLike { + if (!onfulfilled) { + return this as PromiseLike<{}> as PromiseLike; + } + + switch (this.state) { + case PromiseState.Unresolved: { + const res = PromiseImpl.deferred(); + const handle = (value: TResult1 | TResult2 | PromiseLike): void => { + if (isPromiseLike(value)) { + value.then(v => res.resolve(v), e => res.reject(e)); + } + else { + res.resolve(value); + } + }; + Debug.assert(!this.callback); + this.callback = { + onfulfilled: value => handle(onfulfilled(value)), + onrejected: err => { + if (onrejected) { + handle(onrejected(err)); + } + else { + res.reject(err); + } + }, + }; + return res; + } + case PromiseState.Success: + return toPromiseLike(onfulfilled(this.result)); + case PromiseState.Failure: + return toPromiseLike(onrejected(this.error)); + } + } + } + + function isPromiseLike(x: T | PromiseLike): x is PromiseLike { + return !!(x as any).then; + } + + function toPromiseLike(x: T | PromiseLike): PromiseLike { + return isPromiseLike(x) ? x as PromiseLike : PromiseImpl.resolved(x as T); + } } // Display-part writer helpers diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index b1595030df088..726dd3c00c264 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -14,6 +14,20 @@ and limitations under the License. ***************************************************************************** */ declare namespace ts { + interface EmitOutput { + outputFiles: OutputFile[]; + emitSkipped: boolean; + } + interface EmitOutputDetailed extends EmitOutput { + diagnostics: Diagnostic[]; + sourceMaps: SourceMapData[]; + emittedSourceFiles: SourceFile[]; + } + interface OutputFile { + name: string; + writeByteOrderMark: boolean; + text: string; + } /** * Type of objects whose values are all of the same type. * The `in` and `for-in` operators can *not* be safely used, @@ -3205,10 +3219,11 @@ declare namespace ts { }; } declare namespace ts { - function getEffectiveTypeRoots(options: CompilerOptions, host: { + interface GetEffectiveTypeRootsHost { directoryExists?: (directoryName: string) => boolean; getCurrentDirectory?: () => string; - }): string[] | undefined; + } + function getEffectiveTypeRoots(options: CompilerOptions, host: GetEffectiveTypeRootsHost): string[] | undefined; /** * @param {string | undefined} containingFile - file that contains type reference directive, can be undefined if containing file is unknown. * This is possible in case if resolution is performed for directives specified via 'types' parameter. In this case initial path for secondary lookups @@ -3246,6 +3261,20 @@ declare namespace ts { function resolveModuleName(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: ModuleResolutionCache): ResolvedModuleWithFailedLookupLocations; function nodeModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: ModuleResolutionCache): ResolvedModuleWithFailedLookupLocations; function classicNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: NonRelativeModuleNameResolutionCache): ResolvedModuleWithFailedLookupLocations; + enum PackageNameValidationResult { + Ok = 0, + ScopedPackagesNotSupported = 1, + EmptyName = 2, + NameTooLong = 3, + NameStartsWithDot = 4, + NameStartsWithUnderscore = 5, + NameContainsNonURISafeCharacters = 6, + } + /** + * Validates package name using rules defined at https://docs.npmjs.com/files/package.json + */ + function validatePackageName(packageName: string): PackageNameValidationResult; + function renderPackageNameValidationFailure(result: PackageNameValidationResult, typing: string): string; } declare namespace ts { function createNodeArray(elements?: ReadonlyArray, hasTrailingComma?: boolean): NodeArray; @@ -3723,20 +3752,6 @@ declare namespace ts { function createPrinter(printerOptions?: PrinterOptions, handlers?: PrintHandlers): Printer; } declare namespace ts { - interface EmitOutput { - outputFiles: OutputFile[]; - emitSkipped: boolean; - } - interface EmitOutputDetailed extends EmitOutput { - diagnostics: Diagnostic[]; - sourceMaps: SourceMapData[]; - emittedSourceFiles: SourceFile[]; - } - interface OutputFile { - name: string; - writeByteOrderMark: boolean; - text: string; - } function getFileEmitOutput(program: Program, sourceFile: SourceFile, emitOnlyDtsFiles: boolean, isDetailed: boolean, cancellationToken?: CancellationToken, customTransformers?: CustomTransformers): EmitOutput | EmitOutputDetailed; } declare namespace ts { @@ -3866,7 +3881,11 @@ declare namespace ts { interface HostCancellationToken { isCancellationRequested(): boolean; } - interface LanguageServiceHost { + interface InstallPackageOptions { + fileName: Path; + packageName: string; + } + interface LanguageServiceHost extends GetEffectiveTypeRootsHost { getCompilationSettings(): CompilerOptions; getNewLine?(): string; getProjectVersion?(): string; @@ -3876,7 +3895,6 @@ declare namespace ts { getScriptSnapshot(fileName: string): IScriptSnapshot | undefined; getLocalizedDiagnosticMessages?(): any; getCancellationToken?(): HostCancellationToken; - getCurrentDirectory(): string; getDefaultLibFileName(options: CompilerOptions): string; log?(s: string): void; trace?(s: string): void; @@ -3888,12 +3906,13 @@ declare namespace ts { getTypeRootsVersion?(): number; resolveModuleNames?(moduleNames: string[], containingFile: string, reusedNames?: string[]): ResolvedModule[]; resolveTypeReferenceDirectives?(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[]; - directoryExists?(directoryName: string): boolean; getDirectories?(directoryName: string): string[]; /** * Gets a set of custom transformers to use during emit. */ getCustomTransformers?(): CustomTransformers | undefined; + tryGetTypesRegistry?(): Map | undefined; + installPackage?(options: InstallPackageOptions): PromiseLike; } interface LanguageService { cleanupSemanticCache(): void; @@ -3941,6 +3960,7 @@ declare namespace ts { isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): boolean; getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): TextSpan; getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: number[], formatOptions: FormatCodeSettings): CodeAction[]; + applyCodeActionCommand(fileName: string, action: CodeActionCommand): PromiseLike; getApplicableRefactors(fileName: string, positionOrRaneg: number | TextRange): ApplicableRefactorInfo[]; getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string): RefactorEditInfo | undefined; getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean): EmitOutput; @@ -3948,6 +3968,9 @@ declare namespace ts { getProgram(): Program; dispose(): void; } + interface ApplyCodeActionCommandResult { + successMessage: string; + } interface Classifications { spans: number[]; endOfLineState: EndOfLineState; @@ -4012,6 +4035,14 @@ declare namespace ts { description: string; /** Text changes to apply to each file as part of the code action */ changes: FileTextChanges[]; + /** + * If the user accepts the code fix, the editor should send the action back in a `applyAction` request. + * This allows the language service to have side effects (e.g. installing dependencies) upon a code fix. + */ + commands?: CodeActionCommand[]; + } + type CodeActionCommand = InstallPackageAction; + interface InstallPackageAction { } /** * A set of one or more available refactoring actions, grouped under a parent refactoring. @@ -4060,6 +4091,7 @@ declare namespace ts { edits: FileTextChanges[]; renameFilename: string | undefined; renameLocation: number | undefined; + commands?: CodeActionCommand[]; } interface TextInsertion { newText: string; @@ -4628,11 +4660,10 @@ declare namespace ts.server { interface SortedReadonlyArray extends ReadonlyArray { " __sortedArrayBrand": any; } - interface TypingInstallerRequest { + interface TypingInstallerRequestWithProjectName { readonly projectName: string; - readonly kind: "discover" | "closeProject"; } - interface DiscoverTypings extends TypingInstallerRequest { + interface DiscoverTypings extends TypingInstallerRequestWithProjectName { readonly fileNames: string[]; readonly projectRootPath: Path; readonly compilerOptions: CompilerOptions; @@ -4641,16 +4672,25 @@ declare namespace ts.server { readonly cachePath?: string; readonly kind: "discover"; } - interface CloseProject extends TypingInstallerRequest { + interface CloseProject extends TypingInstallerRequestWithProjectName { readonly kind: "closeProject"; } + interface TypesRegistryRequest { + readonly kind: "typesRegistry"; + } + interface InstallPackageRequest extends InstallPackageOptions { + readonly kind: "installPackage"; + readonly projectRootPath: Path; + } type ActionSet = "action::set"; type ActionInvalidate = "action::invalidate"; + type EventTypesRegistry = "event::typesRegistry"; + type EventPackageInstalled = "event::packageInstalled"; type EventBeginInstallTypes = "event::beginInstallTypes"; type EventEndInstallTypes = "event::endInstallTypes"; type EventInitializationFailed = "event::initializationFailed"; interface TypingInstallerResponse { - readonly kind: ActionSet | ActionInvalidate | EventBeginInstallTypes | EventEndInstallTypes | EventInitializationFailed; + readonly kind: ActionSet | ActionInvalidate | EventTypesRegistry | EventPackageInstalled | EventBeginInstallTypes | EventEndInstallTypes | EventInitializationFailed; } interface InitializationFailedResponse extends TypingInstallerResponse { readonly kind: EventInitializationFailed; @@ -4686,6 +4726,8 @@ declare namespace ts.server { declare namespace ts.server { const ActionSet: ActionSet; const ActionInvalidate: ActionInvalidate; + const EventTypesRegistry: EventTypesRegistry; + const EventPackageInstalled: EventPackageInstalled; const EventBeginInstallTypes: EventBeginInstallTypes; const EventEndInstallTypes: EventEndInstallTypes; const EventInitializationFailed: EventInitializationFailed; @@ -4823,6 +4865,7 @@ declare namespace ts.server.protocol { DocCommentTemplate = "docCommentTemplate", CompilerOptionsForInferredProjects = "compilerOptionsForInferredProjects", GetCodeFixes = "getCodeFixes", + ApplyCodeActionCommand = "applyCodeActionCommand", GetSupportedCodeFixes = "getSupportedCodeFixes", GetApplicableRefactors = "getApplicableRefactors", GetEditsForRefactor = "getEditsForRefactor", @@ -4844,6 +4887,7 @@ declare namespace ts.server.protocol { * Client-initiated request message */ interface Request extends Message { + type: "request"; /** * The command to execute */ @@ -4863,6 +4907,7 @@ declare namespace ts.server.protocol { * Server-initiated event message */ interface Event extends Message { + type: "event"; /** * Name of event */ @@ -4876,6 +4921,7 @@ declare namespace ts.server.protocol { * Response by server to client request message. */ interface Response extends Message { + type: "response"; /** * Sequence number of the request message. */ @@ -4889,7 +4935,8 @@ declare namespace ts.server.protocol { */ command: string; /** - * Contains error message if success === false. + * If success === false, this should always be provided. + * Otherwise, may (or may not) contain a success message. */ message?: string; /** @@ -5165,6 +5212,12 @@ declare namespace ts.server.protocol { command: CommandTypes.GetCodeFixes; arguments: CodeFixRequestArgs; } + interface ApplyCodeActionCommandRequest extends Request { + command: CommandTypes.ApplyCodeActionCommand; + arguments: ApplyCodeActionCommandRequestArgs; + } + interface ApplyCodeActionCommandResponse extends Response { + } interface FileRangeRequestArgs extends FileRequestArgs { /** * The line number for the request (1-based). @@ -5192,6 +5245,9 @@ declare namespace ts.server.protocol { */ errorCodes?: number[]; } + interface ApplyCodeActionCommandRequestArgs extends FileRequestArgs { + command: {}; + } /** * Response for GetCodeFixes request. */ @@ -5930,6 +5986,7 @@ declare namespace ts.server.protocol { description: string; /** Text changes to apply to each file as part of the code action */ changes: FileCodeEdits[]; + commands?: {}[]; } /** * Format and format on key response message. @@ -6837,7 +6894,7 @@ declare namespace ts.server { logError(err: Error, cmd: string): void; send(msg: protocol.Message): void; event(info: T, eventName: string): void; - output(info: any, cmdName: string, reqSeq?: number, errorMsg?: string): void; + output(info: {} | undefined, cmdName: string, reqSeq: number, success: boolean, message?: string): void; private semanticCheck(file, project); private syntacticCheck(file, project); private updateErrorCheck(next, checkList, ms, requireOpen?); @@ -6913,8 +6970,9 @@ declare namespace ts.server { private getApplicableRefactors(args); private getEditsForRefactor(args, simplifiedResult); private getCodeFixes(args, simplifiedResult); + private applyCodeActionCommand(commandName, requestSeq, args); private getStartAndEndPosition(args, scriptInfo); - private mapCodeAction(codeAction, scriptInfo); + private mapCodeAction({description, changes: unmappedChanges, commands}, scriptInfo); private mapTextChangesToCodeEdits(project, textChanges); private convertTextChangeToCodeEdit(change, scriptInfo); private getBraceMatching(args, simplifiedResult); @@ -6924,19 +6982,17 @@ declare namespace ts.server { private notRequired(); private requiredResponse(response); private handlers; - addProtocolHandler(command: string, handler: (request: protocol.Request) => { - response?: any; - responseRequired: boolean; - }): void; + addProtocolHandler(command: string, handler: (request: protocol.Request) => HandlerResponse): void; private setCurrentRequest(requestId); private resetCurrentRequest(requestId); executeWithRequestId(requestId: number, f: () => T): T; - executeCommand(request: protocol.Request): { - response?: any; - responseRequired?: boolean; - }; + executeCommand(request: protocol.Request): HandlerResponse; onMessage(message: string): void; } + interface HandlerResponse { + response?: {}; + responseRequired?: boolean; + } } declare namespace ts.server { class ScriptInfo { @@ -7001,7 +7057,12 @@ declare namespace ts { function updateWatchingWildcardDirectories(existingWatchedForWildcards: Map, wildcardDirectories: Map, watchDirectory: (directory: string, flags: WatchDirectoryFlags) => FileWatcher): void; } declare namespace ts.server { + interface InstallPackageOptionsWithProjectRootPath extends InstallPackageOptions { + projectRootPath: Path; + } interface ITypingsInstaller { + tryGetTypesRegistry(): Map | undefined; + installPackage(options: InstallPackageOptionsWithProjectRootPath): PromiseLike; enqueueInstallTypingsRequest(p: Project, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray): void; attach(projectService: ProjectService): void; onProjectClosed(p: Project): void; @@ -7012,6 +7073,8 @@ declare namespace ts.server { private readonly installer; private readonly perProjectCache; constructor(installer: ITypingsInstaller); + tryGetTypesRegistry(): Map | undefined; + installPackage(options: InstallPackageOptionsWithProjectRootPath): PromiseLike; getTypingsForProject(project: Project, unresolvedImports: SortedReadonlyArray, forceRefresh: boolean): SortedReadonlyArray; updateTypingsForProject(projectName: string, compilerOptions: CompilerOptions, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray, newTypings: string[]): void; deleteTypingsForProject(projectName: string): void; @@ -7106,6 +7169,9 @@ declare namespace ts.server { isJsOnlyProject(): boolean; getCachedUnresolvedImportsPerFile_TestOnly(): UnresolvedImportsMap; static resolveModule(moduleName: string, initialDir: string, host: ServerHost, log: (message: string) => void): {}; + tryGetTypesRegistry(): Map | undefined; + installPackage(options: InstallPackageOptions): PromiseLike; + private readonly typingsCache; getCompilationSettings(): CompilerOptions; getNewLine(): string; getProjectVersion(): string; @@ -7245,6 +7311,7 @@ declare namespace ts.server { class ExternalProject extends Project { externalProjectName: string; compileOnSaveEnabled: boolean; + private readonly projectFilePath; excludedFiles: ReadonlyArray; private typeAcquisition; getExcludedFiles(): ReadonlyArray; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 0c74f74c7413c..f332386932eb0 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -14,6 +14,20 @@ and limitations under the License. ***************************************************************************** */ declare namespace ts { + interface EmitOutput { + outputFiles: OutputFile[]; + emitSkipped: boolean; + } + interface EmitOutputDetailed extends EmitOutput { + diagnostics: Diagnostic[]; + sourceMaps: SourceMapData[]; + emittedSourceFiles: SourceFile[]; + } + interface OutputFile { + name: string; + writeByteOrderMark: boolean; + text: string; + } /** * Type of objects whose values are all of the same type. * The `in` and `for-in` operators can *not* be safely used, @@ -3152,10 +3166,11 @@ declare namespace ts { function updateSourceFile(sourceFile: SourceFile, newText: string, textChangeRange: TextChangeRange, aggressiveChecks?: boolean): SourceFile; } declare namespace ts { - function getEffectiveTypeRoots(options: CompilerOptions, host: { + interface GetEffectiveTypeRootsHost { directoryExists?: (directoryName: string) => boolean; getCurrentDirectory?: () => string; - }): string[] | undefined; + } + function getEffectiveTypeRoots(options: CompilerOptions, host: GetEffectiveTypeRootsHost): string[] | undefined; /** * @param {string | undefined} containingFile - file that contains type reference directive, can be undefined if containing file is unknown. * This is possible in case if resolution is performed for directives specified via 'types' parameter. In this case initial path for secondary lookups @@ -3193,6 +3208,20 @@ declare namespace ts { function resolveModuleName(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: ModuleResolutionCache): ResolvedModuleWithFailedLookupLocations; function nodeModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: ModuleResolutionCache): ResolvedModuleWithFailedLookupLocations; function classicNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: NonRelativeModuleNameResolutionCache): ResolvedModuleWithFailedLookupLocations; + enum PackageNameValidationResult { + Ok = 0, + ScopedPackagesNotSupported = 1, + EmptyName = 2, + NameTooLong = 3, + NameStartsWithDot = 4, + NameStartsWithUnderscore = 5, + NameContainsNonURISafeCharacters = 6, + } + /** + * Validates package name using rules defined at https://docs.npmjs.com/files/package.json + */ + function validatePackageName(packageName: string): PackageNameValidationResult; + function renderPackageNameValidationFailure(result: PackageNameValidationResult, typing: string): string; } declare namespace ts { function createNodeArray(elements?: ReadonlyArray, hasTrailingComma?: boolean): NodeArray; @@ -3670,20 +3699,6 @@ declare namespace ts { function createPrinter(printerOptions?: PrinterOptions, handlers?: PrintHandlers): Printer; } declare namespace ts { - interface EmitOutput { - outputFiles: OutputFile[]; - emitSkipped: boolean; - } - interface EmitOutputDetailed extends EmitOutput { - diagnostics: Diagnostic[]; - sourceMaps: SourceMapData[]; - emittedSourceFiles: SourceFile[]; - } - interface OutputFile { - name: string; - writeByteOrderMark: boolean; - text: string; - } function getFileEmitOutput(program: Program, sourceFile: SourceFile, emitOnlyDtsFiles: boolean, isDetailed: boolean, cancellationToken?: CancellationToken, customTransformers?: CustomTransformers): EmitOutput | EmitOutputDetailed; } declare namespace ts { @@ -3866,7 +3881,11 @@ declare namespace ts { interface HostCancellationToken { isCancellationRequested(): boolean; } - interface LanguageServiceHost { + interface InstallPackageOptions { + fileName: Path; + packageName: string; + } + interface LanguageServiceHost extends GetEffectiveTypeRootsHost { getCompilationSettings(): CompilerOptions; getNewLine?(): string; getProjectVersion?(): string; @@ -3876,7 +3895,6 @@ declare namespace ts { getScriptSnapshot(fileName: string): IScriptSnapshot | undefined; getLocalizedDiagnosticMessages?(): any; getCancellationToken?(): HostCancellationToken; - getCurrentDirectory(): string; getDefaultLibFileName(options: CompilerOptions): string; log?(s: string): void; trace?(s: string): void; @@ -3888,12 +3906,13 @@ declare namespace ts { getTypeRootsVersion?(): number; resolveModuleNames?(moduleNames: string[], containingFile: string, reusedNames?: string[]): ResolvedModule[]; resolveTypeReferenceDirectives?(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[]; - directoryExists?(directoryName: string): boolean; getDirectories?(directoryName: string): string[]; /** * Gets a set of custom transformers to use during emit. */ getCustomTransformers?(): CustomTransformers | undefined; + tryGetTypesRegistry?(): Map | undefined; + installPackage?(options: InstallPackageOptions): PromiseLike; } interface LanguageService { cleanupSemanticCache(): void; @@ -3941,6 +3960,7 @@ declare namespace ts { isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): boolean; getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): TextSpan; getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: number[], formatOptions: FormatCodeSettings): CodeAction[]; + applyCodeActionCommand(fileName: string, action: CodeActionCommand): PromiseLike; getApplicableRefactors(fileName: string, positionOrRaneg: number | TextRange): ApplicableRefactorInfo[]; getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string): RefactorEditInfo | undefined; getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean): EmitOutput; @@ -3948,6 +3968,9 @@ declare namespace ts { getProgram(): Program; dispose(): void; } + interface ApplyCodeActionCommandResult { + successMessage: string; + } interface Classifications { spans: number[]; endOfLineState: EndOfLineState; @@ -4012,6 +4035,14 @@ declare namespace ts { description: string; /** Text changes to apply to each file as part of the code action */ changes: FileTextChanges[]; + /** + * If the user accepts the code fix, the editor should send the action back in a `applyAction` request. + * This allows the language service to have side effects (e.g. installing dependencies) upon a code fix. + */ + commands?: CodeActionCommand[]; + } + type CodeActionCommand = InstallPackageAction; + interface InstallPackageAction { } /** * A set of one or more available refactoring actions, grouped under a parent refactoring. @@ -4060,6 +4091,7 @@ declare namespace ts { edits: FileTextChanges[]; renameFilename: string | undefined; renameLocation: number | undefined; + commands?: CodeActionCommand[]; } interface TextInsertion { newText: string; diff --git a/tests/cases/fourslash/codeFixCannotFindModule.ts b/tests/cases/fourslash/codeFixCannotFindModule.ts new file mode 100644 index 0000000000000..a4a9fd56006d9 --- /dev/null +++ b/tests/cases/fourslash/codeFixCannotFindModule.ts @@ -0,0 +1,15 @@ +/// + +////import * as abs from "abs"; + +test.setTypesRegistry({ + "abs": undefined, +}); + +verify.codeFixAvailable([{ + description: "Install typings for abs", + commands: [{ + type: "install package", + packageName: "@types/abs", + }], +}]); diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index f4d47abc9c9c4..ac1764b1a57ea 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -118,6 +118,7 @@ declare namespace FourSlashInterface { rangesByText(): ts.Map; markerByName(s: string): Marker; symbolsInScope(range: Range): any[]; + setTypesRegistry(map: { [key: string]: void }): void; } class goTo { marker(name?: string | Marker): void; @@ -162,12 +163,17 @@ declare namespace FourSlashInterface { errorCode?: number, index?: number, }); - codeFixAvailable(): void; + codeFixAvailable(options: Array<{ description: string, actions: Array<{ type: string, data: {} }> }>): void; applicableRefactorAvailableAtMarker(markerName: string): void; codeFixDiagnosticsAvailableAtMarkers(markerNames: string[], diagnosticCode?: number): void; applicableRefactorAvailableForRange(): void; - refactorAvailable(name: string, actionName?: string); + refactorAvailable(name: string, actionName?: string): void; + refactor(options: { + name: string; + actionName: string; + refactors: any[]; + }): void; } class verify extends verifyNegatable { assertHasRanges(ranges: Range[]): void; @@ -256,6 +262,7 @@ declare namespace FourSlashInterface { fileAfterApplyingRefactorAtMarker(markerName: string, expectedContent: string, refactorNameToApply: string, actionName: string, formattingOptions?: FormatCodeOptions): void; rangeIs(expectedText: string, includeWhiteSpace?: boolean): void; fileAfterApplyingRefactorAtMarker(markerName: string, expectedContent: string, refactorNameToApply: string, formattingOptions?: FormatCodeOptions): void; + getAndApplyCodeFix(errorCode?: number, index?: number): void; importFixAtPosition(expectedTextArray: string[], errorCode?: number): void; navigationBar(json: any, options?: { checkSpans?: boolean }): void; diff --git a/tests/cases/fourslash/refactorInstallTypesForPackage.ts b/tests/cases/fourslash/refactorInstallTypesForPackage.ts new file mode 100644 index 0000000000000..edcdc2ba6e0c9 --- /dev/null +++ b/tests/cases/fourslash/refactorInstallTypesForPackage.ts @@ -0,0 +1,25 @@ +/// + +////import * as abs from "/*a*/abs/subModule/*b*/"; + +test.setTypesRegistry({ + "abs": undefined, +}); + +goTo.select("a", "b"); +verify.refactor({ + name: "Install missing types package", + actionName: "install", + refactors: [ + { + name: "Install missing types package", + description: "Install missing types package", + actions: [ + { + description: "Install '@types/abs'", + name: "install", + } + ] + } + ], +}); diff --git a/tests/cases/fourslash/refactorInstallTypesForPackage_importEquals.ts b/tests/cases/fourslash/refactorInstallTypesForPackage_importEquals.ts new file mode 100644 index 0000000000000..18793e4b353dd --- /dev/null +++ b/tests/cases/fourslash/refactorInstallTypesForPackage_importEquals.ts @@ -0,0 +1,25 @@ +/// + +////import abs = require("/*a*/abs/subModule/*b*/"); + +test.setTypesRegistry({ + "abs": undefined, +}); + +goTo.select("a", "b"); +verify.refactor({ + name: "Install missing types package", + actionName: "install", + refactors: [ + { + name: "Install missing types package", + description: "Install missing types package", + actions: [ + { + description: "Install '@types/abs'", + name: "install", + } + ] + } + ], +});