diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 20c3562271c7b..5f02a81ff50d6 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -311,8 +311,8 @@ namespace ts { } /** Works like Array.prototype.findIndex, returning `-1` if no element satisfying the predicate is found. */ - export function findIndex(array: ReadonlyArray, predicate: (element: T, index: number) => boolean): number { - for (let i = 0; i < array.length; i++) { + export function findIndex(array: ReadonlyArray, predicate: (element: T, index: number) => boolean, startIndex?: number): number { + for (let i = startIndex || 0; i < array.length; i++) { if (predicate(array[i], i)) { return i; } @@ -2293,20 +2293,24 @@ namespace ts { return ["", ...relative, ...components]; } + export function getRelativePathFromFile(from: string, to: string, getCanonicalFileName: GetCanonicalFileName) { + return ensurePathIsNonModuleName(getRelativePathFromDirectory(getDirectoryPath(from), to, getCanonicalFileName)); + } + /** * Gets a relative path that can be used to traverse between `from` and `to`. */ - export function getRelativePath(from: string, to: string, ignoreCase: boolean): string; + export function getRelativePathFromDirectory(from: string, to: string, ignoreCase: boolean): string; /** * Gets a relative path that can be used to traverse between `from` and `to`. */ // tslint:disable-next-line:unified-signatures - export function getRelativePath(from: string, to: string, getCanonicalFileName: GetCanonicalFileName): string; - export function getRelativePath(from: string, to: string, getCanonicalFileNameOrIgnoreCase: GetCanonicalFileName | boolean) { - Debug.assert((getRootLength(from) > 0) === (getRootLength(to) > 0), "Paths must either both be absolute or both be relative"); + export function getRelativePathFromDirectory(fromDirectory: string, to: string, getCanonicalFileName: GetCanonicalFileName): string; + export function getRelativePathFromDirectory(fromDirectory: string, to: string, getCanonicalFileNameOrIgnoreCase: GetCanonicalFileName | boolean) { + Debug.assert((getRootLength(fromDirectory) > 0) === (getRootLength(to) > 0), "Paths must either both be absolute or both be relative"); const getCanonicalFileName = typeof getCanonicalFileNameOrIgnoreCase === "function" ? getCanonicalFileNameOrIgnoreCase : identity; const ignoreCase = typeof getCanonicalFileNameOrIgnoreCase === "boolean" ? getCanonicalFileNameOrIgnoreCase : false; - const pathComponents = getPathComponentsRelativeTo(from, to, ignoreCase ? equateStringsCaseInsensitive : equateStringsCaseSensitive, getCanonicalFileName); + const pathComponents = getPathComponentsRelativeTo(fromDirectory, to, ignoreCase ? equateStringsCaseInsensitive : equateStringsCaseSensitive, getCanonicalFileName); return getPathFromPathComponents(pathComponents); } diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 1e101ce6e766b..aa62360ba9dd5 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -4236,5 +4236,9 @@ "Convert all 'require' to 'import'": { "category": "Message", "code": 95048 + }, + "Move to a new file": { + "category": "Message", + "code": 95049 } } diff --git a/src/compiler/factory.ts b/src/compiler/factory.ts index e8916ac62e51e..7dc73a85587b2 100644 --- a/src/compiler/factory.ts +++ b/src/compiler/factory.ts @@ -1497,6 +1497,13 @@ namespace ts { return block; } + /* @internal */ + export function createExpressionStatement(expression: Expression): ExpressionStatement { + const node = createSynthesizedNode(SyntaxKind.ExpressionStatement); + node.expression = expression; + return node; + } + export function updateBlock(node: Block, statements: ReadonlyArray) { return node.statements !== statements ? updateNode(createBlock(statements, node.multiLine), node) @@ -1523,9 +1530,7 @@ namespace ts { } export function createStatement(expression: Expression) { - const node = createSynthesizedNode(SyntaxKind.ExpressionStatement); - node.expression = parenthesizeExpressionForExpressionStatement(expression); - return node; + return createExpressionStatement(parenthesizeExpressionForExpressionStatement(expression)); } export function updateStatement(node: ExpressionStatement, expression: Expression) { diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index 5e7dede93ee53..a9a88f3db2a2c 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -3005,9 +3005,7 @@ Actual: ${stringify(fullActual)}`); } public verifyApplicableRefactorAvailableAtMarker(negative: boolean, markerName: string) { - const marker = this.getMarkerByName(markerName); - const applicableRefactors = this.languageService.getApplicableRefactors(this.activeFile.fileName, marker.position, ts.defaultPreferences); - const isAvailable = applicableRefactors && applicableRefactors.length > 0; + const isAvailable = this.getApplicableRefactors(this.getMarkerByName(markerName).position).length > 0; if (negative && isAvailable) { this.raiseError(`verifyApplicableRefactorAvailableAtMarker failed - expected no refactor at marker ${markerName} but found some.`); } @@ -3024,9 +3022,7 @@ Actual: ${stringify(fullActual)}`); } public verifyRefactorAvailable(negative: boolean, name: string, actionName?: string) { - const selection = this.getSelection(); - - let refactors = this.languageService.getApplicableRefactors(this.activeFile.fileName, selection, ts.defaultPreferences) || []; + let refactors = this.getApplicableRefactors(this.getSelection()); refactors = refactors.filter(r => r.name === name && (actionName === undefined || r.actions.some(a => a.name === actionName))); const isAvailable = refactors.length > 0; @@ -3046,10 +3042,7 @@ Actual: ${stringify(fullActual)}`); } public verifyRefactor({ name, actionName, refactors }: FourSlashInterface.VerifyRefactorOptions) { - const selection = this.getSelection(); - - const actualRefactors = (this.languageService.getApplicableRefactors(this.activeFile.fileName, selection, ts.defaultPreferences) || ts.emptyArray) - .filter(r => r.name === name && r.actions.some(a => a.name === actionName)); + const actualRefactors = this.getApplicableRefactors(this.getSelection()).filter(r => r.name === name && r.actions.some(a => a.name === actionName)); this.assertObjectsEqual(actualRefactors, refactors); } @@ -3059,8 +3052,7 @@ Actual: ${stringify(fullActual)}`); throw new Error("Exactly one refactor range is allowed per test."); } - const applicableRefactors = this.languageService.getApplicableRefactors(this.activeFile.fileName, ts.first(ranges), ts.defaultPreferences); - const isAvailable = applicableRefactors && applicableRefactors.length > 0; + const isAvailable = this.getApplicableRefactors(ts.first(ranges)).length > 0; if (negative && isAvailable) { this.raiseError(`verifyApplicableRefactorAvailableForRange failed - expected no refactor but found some.`); } @@ -3071,7 +3063,7 @@ Actual: ${stringify(fullActual)}`); public applyRefactor({ refactorName, actionName, actionDescription, newContent: newContentWithRenameMarker }: FourSlashInterface.ApplyRefactorOptions) { const range = this.getSelection(); - const refactors = this.languageService.getApplicableRefactors(this.activeFile.fileName, range, ts.defaultPreferences); + const refactors = this.getApplicableRefactors(range); const refactorsWithName = refactors.filter(r => r.name === refactorName); if (refactorsWithName.length === 0) { this.raiseError(`The expected refactor: ${refactorName} is not available at the marker location.\nAvailable refactors: ${refactors.map(r => r.name)}`); @@ -3117,7 +3109,48 @@ Actual: ${stringify(fullActual)}`); return { renamePosition, newContent }; } } + } + public noMoveToNewFile() { + for (const range of this.getRanges()) { + for (const refactor of this.getApplicableRefactors(range, { allowTextChangesInNewFiles: true })) { + if (refactor.name === "Move to a new file") { + ts.Debug.fail("Did not expect to get 'move to a new file' refactor"); + } + } + } + } + + public moveToNewFile(options: FourSlashInterface.MoveToNewFileOptions): void { + assert(this.getRanges().length === 1); + const range = this.getRanges()[0]; + const refactor = ts.find(this.getApplicableRefactors(range, { allowTextChangesInNewFiles: true }), r => r.name === "Move to a new file"); + assert(refactor.actions.length === 1); + const action = ts.first(refactor.actions); + assert(action.name === "Move to a new file" && action.description === "Move to a new file"); + + const editInfo = this.languageService.getEditsForRefactor(this.activeFile.fileName, this.formatCodeSettings, range, refactor.name, action.name, ts.defaultPreferences); + for (const edit of editInfo.edits) { + const newContent = options.newFileContents[edit.fileName]; + if (newContent === undefined) { + this.raiseError(`There was an edit in ${edit.fileName} but new content was not specified.`); + } + if (this.testData.files.some(f => f.fileName === edit.fileName)) { + this.applyEdits(edit.fileName, edit.textChanges, /*isFormattingEdit*/ false); + this.openFile(edit.fileName); + this.verifyCurrentFileContent(newContent); + } + else { + assert(edit.textChanges.length === 1); + const change = ts.first(edit.textChanges); + assert.deepEqual(change.span, ts.createTextSpan(0, 0)); + assert.equal(change.newText, newContent, `Content for ${edit.fileName}`); + } + } + + for (const fileName in options.newFileContents) { + assert(editInfo.edits.some(e => e.fileName === fileName)); + } } public verifyFileAfterApplyingRefactorAtMarker( @@ -3333,6 +3366,10 @@ Actual: ${stringify(fullActual)}`); this.verifyCurrentFileContent(options.newFileContents[fileName]); } } + + private getApplicableRefactors(positionOrRange: number | ts.TextRange, preferences = ts.defaultPreferences): ReadonlyArray { + return this.languageService.getApplicableRefactors(this.activeFile.fileName, positionOrRange, preferences) || ts.emptyArray; + } } export function runFourSlashTest(basePath: string, testType: FourSlashTestType, fileName: string) { @@ -4430,6 +4467,13 @@ namespace FourSlashInterface { public getEditsForFileRename(options: GetEditsForFileRenameOptions) { this.state.getEditsForFileRename(options); } + + public moveToNewFile(options: MoveToNewFileOptions): void { + this.state.moveToNewFile(options); + } + public noMoveToNewFile(): void { + this.state.noMoveToNewFile(); + } } export class Edit { @@ -4803,4 +4847,8 @@ namespace FourSlashInterface { readonly newPath: string; readonly newFileContents: { readonly [fileName: string]: string }; } + + export interface MoveToNewFileOptions { + readonly newFileContents: { readonly [fileName: string]: string }; + } } diff --git a/src/harness/tsconfig.json b/src/harness/tsconfig.json index fd1d66f394dcf..a9a0f9176fbfb 100644 --- a/src/harness/tsconfig.json +++ b/src/harness/tsconfig.json @@ -116,6 +116,7 @@ "../services/codefixes/useDefaultImport.ts", "../services/refactors/extractSymbol.ts", "../services/refactors/generateGetAccessorAndSetAccessor.ts", + "../services/refactors/moveToNewFile.ts", "../services/sourcemaps.ts", "../services/services.ts", "../services/breakpoints.ts", diff --git a/src/harness/unittests/paths.ts b/src/harness/unittests/paths.ts index 0cd90be0483d0..0b28bedc929af 100644 --- a/src/harness/unittests/paths.ts +++ b/src/harness/unittests/paths.ts @@ -268,25 +268,25 @@ describe("core paths", () => { assert.strictEqual(ts.resolvePath("a", "b", "../c"), "a/c"); }); it("getPathRelativeTo", () => { - assert.strictEqual(ts.getRelativePath("/", "/", /*ignoreCase*/ false), ""); - assert.strictEqual(ts.getRelativePath("/a", "/a", /*ignoreCase*/ false), ""); - assert.strictEqual(ts.getRelativePath("/a/", "/a", /*ignoreCase*/ false), ""); - assert.strictEqual(ts.getRelativePath("/a", "/", /*ignoreCase*/ false), ".."); - assert.strictEqual(ts.getRelativePath("/a", "/b", /*ignoreCase*/ false), "../b"); - assert.strictEqual(ts.getRelativePath("/a/b", "/b", /*ignoreCase*/ false), "../../b"); - assert.strictEqual(ts.getRelativePath("/a/b/c", "/b", /*ignoreCase*/ false), "../../../b"); - assert.strictEqual(ts.getRelativePath("/a/b/c", "/b/c", /*ignoreCase*/ false), "../../../b/c"); - assert.strictEqual(ts.getRelativePath("/a/b/c", "/a/b", /*ignoreCase*/ false), ".."); - assert.strictEqual(ts.getRelativePath("c:", "d:", /*ignoreCase*/ false), "d:/"); - assert.strictEqual(ts.getRelativePath("file:///", "file:///", /*ignoreCase*/ false), ""); - assert.strictEqual(ts.getRelativePath("file:///a", "file:///a", /*ignoreCase*/ false), ""); - assert.strictEqual(ts.getRelativePath("file:///a/", "file:///a", /*ignoreCase*/ false), ""); - assert.strictEqual(ts.getRelativePath("file:///a", "file:///", /*ignoreCase*/ false), ".."); - assert.strictEqual(ts.getRelativePath("file:///a", "file:///b", /*ignoreCase*/ false), "../b"); - assert.strictEqual(ts.getRelativePath("file:///a/b", "file:///b", /*ignoreCase*/ false), "../../b"); - assert.strictEqual(ts.getRelativePath("file:///a/b/c", "file:///b", /*ignoreCase*/ false), "../../../b"); - assert.strictEqual(ts.getRelativePath("file:///a/b/c", "file:///b/c", /*ignoreCase*/ false), "../../../b/c"); - assert.strictEqual(ts.getRelativePath("file:///a/b/c", "file:///a/b", /*ignoreCase*/ false), ".."); - assert.strictEqual(ts.getRelativePath("file:///c:", "file:///d:", /*ignoreCase*/ false), "file:///d:/"); + assert.strictEqual(ts.getRelativePathFromDirectory("/", "/", /*ignoreCase*/ false), ""); + assert.strictEqual(ts.getRelativePathFromDirectory("/a", "/a", /*ignoreCase*/ false), ""); + assert.strictEqual(ts.getRelativePathFromDirectory("/a/", "/a", /*ignoreCase*/ false), ""); + assert.strictEqual(ts.getRelativePathFromDirectory("/a", "/", /*ignoreCase*/ false), ".."); + assert.strictEqual(ts.getRelativePathFromDirectory("/a", "/b", /*ignoreCase*/ false), "../b"); + assert.strictEqual(ts.getRelativePathFromDirectory("/a/b", "/b", /*ignoreCase*/ false), "../../b"); + assert.strictEqual(ts.getRelativePathFromDirectory("/a/b/c", "/b", /*ignoreCase*/ false), "../../../b"); + assert.strictEqual(ts.getRelativePathFromDirectory("/a/b/c", "/b/c", /*ignoreCase*/ false), "../../../b/c"); + assert.strictEqual(ts.getRelativePathFromDirectory("/a/b/c", "/a/b", /*ignoreCase*/ false), ".."); + assert.strictEqual(ts.getRelativePathFromDirectory("c:", "d:", /*ignoreCase*/ false), "d:/"); + assert.strictEqual(ts.getRelativePathFromDirectory("file:///", "file:///", /*ignoreCase*/ false), ""); + assert.strictEqual(ts.getRelativePathFromDirectory("file:///a", "file:///a", /*ignoreCase*/ false), ""); + assert.strictEqual(ts.getRelativePathFromDirectory("file:///a/", "file:///a", /*ignoreCase*/ false), ""); + assert.strictEqual(ts.getRelativePathFromDirectory("file:///a", "file:///", /*ignoreCase*/ false), ".."); + assert.strictEqual(ts.getRelativePathFromDirectory("file:///a", "file:///b", /*ignoreCase*/ false), "../b"); + assert.strictEqual(ts.getRelativePathFromDirectory("file:///a/b", "file:///b", /*ignoreCase*/ false), "../../b"); + assert.strictEqual(ts.getRelativePathFromDirectory("file:///a/b/c", "file:///b", /*ignoreCase*/ false), "../../../b"); + assert.strictEqual(ts.getRelativePathFromDirectory("file:///a/b/c", "file:///b/c", /*ignoreCase*/ false), "../../../b/c"); + assert.strictEqual(ts.getRelativePathFromDirectory("file:///a/b/c", "file:///a/b", /*ignoreCase*/ false), ".."); + assert.strictEqual(ts.getRelativePathFromDirectory("file:///c:", "file:///d:", /*ignoreCase*/ false), "file:///d:/"); }); }); \ No newline at end of file diff --git a/src/harness/vpath.ts b/src/harness/vpath.ts index 9b42ba2a28db8..cee41d9df9154 100644 --- a/src/harness/vpath.ts +++ b/src/harness/vpath.ts @@ -18,7 +18,7 @@ namespace vpath { export import dirname = ts.getDirectoryPath; export import basename = ts.getBaseFileName; export import extname = ts.getAnyExtensionFromPath; - export import relative = ts.getRelativePath; + export import relative = ts.getRelativePathFromDirectory; export import beneath = ts.containsPath; export import changeExtension = ts.changeAnyExtension; export import isTypeScript = ts.hasTypeScriptFileExtension; diff --git a/src/server/tsconfig.json b/src/server/tsconfig.json index 71ff00e6ac1ab..87a76a1e3c416 100644 --- a/src/server/tsconfig.json +++ b/src/server/tsconfig.json @@ -112,6 +112,7 @@ "../services/codefixes/useDefaultImport.ts", "../services/refactors/extractSymbol.ts", "../services/refactors/generateGetAccessorAndSetAccessor.ts", + "../services/refactors/moveToNewFile.ts", "../services/sourcemaps.ts", "../services/services.ts", "../services/breakpoints.ts", diff --git a/src/server/tsconfig.library.json b/src/server/tsconfig.library.json index 95489ff0ea357..3dc0402e9a733 100644 --- a/src/server/tsconfig.library.json +++ b/src/server/tsconfig.library.json @@ -118,6 +118,7 @@ "../services/codefixes/useDefaultImport.ts", "../services/refactors/extractSymbol.ts", "../services/refactors/generateGetAccessorAndSetAccessor.ts", + "../services/refactors/moveToNewFile.ts", "../services/sourcemaps.ts", "../services/services.ts", "../services/breakpoints.ts", diff --git a/src/services/breakpoints.ts b/src/services/breakpoints.ts index faab2b08db67c..6815be9863d1a 100644 --- a/src/services/breakpoints.ts +++ b/src/services/breakpoints.ts @@ -41,7 +41,7 @@ namespace ts.BreakpointResolver { } function textSpanEndingAtNextToken(startNode: Node, previousTokenToFindNextEndToken: Node): TextSpan { - return textSpan(startNode, findNextToken(previousTokenToFindNextEndToken, previousTokenToFindNextEndToken.parent)); + return textSpan(startNode, findNextToken(previousTokenToFindNextEndToken, previousTokenToFindNextEndToken.parent, sourceFile)); } function spanInNodeIfStartsOnSameLine(node: Node, otherwiseOnNode?: Node): TextSpan { @@ -60,7 +60,7 @@ namespace ts.BreakpointResolver { } function spanInNextNode(node: Node): TextSpan { - return spanInNode(findNextToken(node, node.parent)); + return spanInNode(findNextToken(node, node.parent, sourceFile)); } function spanInNode(node: Node): TextSpan { diff --git a/src/services/codefixes/convertToEs6Module.ts b/src/services/codefixes/convertToEs6Module.ts index b496697d541d6..57af2edcd017b 100644 --- a/src/services/codefixes/convertToEs6Module.ts +++ b/src/services/codefixes/convertToEs6Module.ts @@ -487,15 +487,6 @@ namespace ts.codefix { : makeImport(/*name*/ undefined, [makeImportSpecifier(propertyName, localName)], moduleSpecifier); } - function makeImport(name: Identifier | undefined, namedImports: ReadonlyArray | undefined, moduleSpecifier: StringLiteralLike): ImportDeclaration { - return makeImportDeclaration(name, namedImports, moduleSpecifier); - } - - export function makeImportDeclaration(name: Identifier, namedImports: ReadonlyArray | undefined, moduleSpecifier: Expression) { - const importClause = (name || namedImports) && createImportClause(name, namedImports && createNamedImports(namedImports)); - return createImportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, importClause, moduleSpecifier); - } - function makeImportSpecifier(propertyName: string | undefined, name: string): ImportSpecifier { return createImportSpecifier(propertyName !== undefined && propertyName !== name ? createIdentifier(propertyName) : undefined, createIdentifier(name)); } diff --git a/src/services/codefixes/fixInvalidImportSyntax.ts b/src/services/codefixes/fixInvalidImportSyntax.ts index 50bf41360bce3..89a9aca461cb5 100644 --- a/src/services/codefixes/fixInvalidImportSyntax.ts +++ b/src/services/codefixes/fixInvalidImportSyntax.ts @@ -28,7 +28,7 @@ namespace ts.codefix { const variations: CodeFixAction[] = []; // import Bluebird from "bluebird"; - variations.push(createAction(context, sourceFile, node, makeImportDeclaration(namespace.name, /*namedImports*/ undefined, node.moduleSpecifier))); + variations.push(createAction(context, sourceFile, node, makeImport(namespace.name, /*namedImports*/ undefined, node.moduleSpecifier))); if (getEmitModuleKind(opts) === ModuleKind.CommonJS) { // import Bluebird = require("bluebird"); diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index 68b6e9a856f62..43fd56ae0b6d9 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -266,7 +266,7 @@ namespace ts.codefix { return [global]; } - const relativePath = removeExtensionAndIndexPostFix(ensurePathIsNonModuleName(getRelativePath(sourceDirectory, moduleFileName, getCanonicalFileName)), moduleResolutionKind, addJsExtension); + const relativePath = removeExtensionAndIndexPostFix(ensurePathIsNonModuleName(getRelativePathFromDirectory(sourceDirectory, moduleFileName, getCanonicalFileName)), moduleResolutionKind, addJsExtension); if (!baseUrl || preferences.importModuleSpecifierPreference === "relative") { return [relativePath]; } @@ -321,7 +321,7 @@ namespace ts.codefix { 1 < 2 = true In this case we should prefer using the relative path "../a" instead of the baseUrl path "foo/a". */ - const pathFromSourceToBaseUrl = ensurePathIsNonModuleName(getRelativePath(sourceDirectory, baseUrl, getCanonicalFileName)); + const pathFromSourceToBaseUrl = ensurePathIsNonModuleName(getRelativePathFromDirectory(sourceDirectory, baseUrl, getCanonicalFileName)); const relativeFirst = getRelativePathNParents(relativePath) < getRelativePathNParents(pathFromSourceToBaseUrl); return relativeFirst ? [relativePath, importRelativeToBaseUrl] : [importRelativeToBaseUrl, relativePath]; }); @@ -390,7 +390,7 @@ namespace ts.codefix { } const normalizedSourcePath = getPathRelativeToRootDirs(sourceDirectory, rootDirs, getCanonicalFileName); - const relativePath = normalizedSourcePath !== undefined ? ensurePathIsNonModuleName(getRelativePath(normalizedSourcePath, normalizedTargetPath, getCanonicalFileName)) : normalizedTargetPath; + const relativePath = normalizedSourcePath !== undefined ? ensurePathIsNonModuleName(getRelativePathFromDirectory(normalizedSourcePath, normalizedTargetPath, getCanonicalFileName)) : normalizedTargetPath; return removeFileExtension(relativePath); } @@ -473,7 +473,7 @@ namespace ts.codefix { return path.substring(parts.topLevelPackageNameIndex + 1); } else { - return ensurePathIsNonModuleName(getRelativePath(sourceDirectory, path, getCanonicalFileName)); + return ensurePathIsNonModuleName(getRelativePathFromDirectory(sourceDirectory, path, getCanonicalFileName)); } } } diff --git a/src/services/codefixes/useDefaultImport.ts b/src/services/codefixes/useDefaultImport.ts index 632cfc97e0075..99fc145b5630a 100644 --- a/src/services/codefixes/useDefaultImport.ts +++ b/src/services/codefixes/useDefaultImport.ts @@ -37,6 +37,6 @@ namespace ts.codefix { } function doChange(changes: textChanges.ChangeTracker, sourceFile: SourceFile, info: Info): void { - changes.replaceNode(sourceFile, info.importNode, makeImportDeclaration(info.name, /*namedImports*/ undefined, info.moduleSpecifier)); + changes.replaceNode(sourceFile, info.importNode, makeImport(info.name, /*namedImports*/ undefined, info.moduleSpecifier)); } } diff --git a/src/services/findAllReferences.ts b/src/services/findAllReferences.ts index bedb657a66d56..4544c3e13d92b 100644 --- a/src/services/findAllReferences.ts +++ b/src/services/findAllReferences.ts @@ -614,27 +614,19 @@ namespace ts.FindAllReferences.Core { checker.getPropertySymbolOfDestructuringAssignment(location); } - function getObjectBindingElementWithoutPropertyName(symbol: Symbol): BindingElement | undefined { + function getObjectBindingElementWithoutPropertyName(symbol: Symbol): BindingElement & { name: Identifier } | undefined { const bindingElement = getDeclarationOfKind(symbol, SyntaxKind.BindingElement); if (bindingElement && bindingElement.parent.kind === SyntaxKind.ObjectBindingPattern && + isIdentifier(bindingElement.name) && !bindingElement.propertyName) { - return bindingElement; + return bindingElement as BindingElement & { name: Identifier }; } } function getPropertySymbolOfObjectBindingPatternWithoutPropertyName(symbol: Symbol, checker: TypeChecker): Symbol | undefined { const bindingElement = getObjectBindingElementWithoutPropertyName(symbol); - if (!bindingElement) return undefined; - - const typeOfPattern = checker.getTypeAtLocation(bindingElement.parent); - const propSymbol = typeOfPattern && checker.getPropertyOfType(typeOfPattern, (bindingElement.name).text); - if (propSymbol && propSymbol.flags & SymbolFlags.Accessor) { - // See GH#16922 - Debug.assert(!!(propSymbol.flags & SymbolFlags.Transient)); - return (propSymbol as TransientSymbol).target; - } - return propSymbol; + return bindingElement && getPropertySymbolFromBindingElement(checker, bindingElement); } /** diff --git a/src/services/formatting/smartIndenter.ts b/src/services/formatting/smartIndenter.ts index 0e89be425328c..a75781b8da0e4 100644 --- a/src/services/formatting/smartIndenter.ts +++ b/src/services/formatting/smartIndenter.ts @@ -269,7 +269,7 @@ namespace ts.formatting { } function nextTokenIsCurlyBraceOnSameLineAsCursor(precedingToken: Node, current: Node, lineAtPosition: number, sourceFile: SourceFile): NextTokenKind { - const nextToken = findNextToken(precedingToken, current); + const nextToken = findNextToken(precedingToken, current, sourceFile); if (!nextToken) { return NextTokenKind.Unknown; } diff --git a/src/services/getEditsForFileRename.ts b/src/services/getEditsForFileRename.ts index d5fc94c6d62ff..f8bcf02d036ee 100644 --- a/src/services/getEditsForFileRename.ts +++ b/src/services/getEditsForFileRename.ts @@ -55,7 +55,7 @@ namespace ts { function getPathUpdater(oldFilePath: string, newFilePath: string, host: LanguageServiceHost): (oldPath: string) => string | undefined { // Get the relative path from old to new location, and append it on to the end of imports and normalize. - const rel = ensurePathIsNonModuleName(getRelativePath(getDirectoryPath(oldFilePath), newFilePath, createGetCanonicalFileName(hostUsesCaseSensitiveFileNames(host)))); + const rel = getRelativePathFromFile(oldFilePath, newFilePath, createGetCanonicalFileName(hostUsesCaseSensitiveFileNames(host))); return oldPath => { if (!pathIsRelative(oldPath)) return; return ensurePathIsNonModuleName(normalizePath(combinePaths(getDirectoryPath(oldPath), rel))); diff --git a/src/services/importTracker.ts b/src/services/importTracker.ts index 4858fdc4b341b..2f84640f57d54 100644 --- a/src/services/importTracker.ts +++ b/src/services/importTracker.ts @@ -267,7 +267,7 @@ namespace ts.FindAllReferences { const { name } = importClause; // If a default import has the same name as the default export, allow to rename it. // Given `import f` and `export default function f`, we will rename both, but for `import g` we will rename just that. - if (name && (!isForRename || name.escapedText === symbolName(exportSymbol))) { + if (name && (!isForRename || name.escapedText === symbolEscapedNameNoDefault(exportSymbol))) { const defaultImportAlias = checker.getSymbolAtLocation(name); addSearch(name, defaultImportAlias); } @@ -550,7 +550,7 @@ namespace ts.FindAllReferences { // If the import has a different name than the export, do not continue searching. // If `importedName` is undefined, do continue searching as the export is anonymous. // (All imports returned from this function will be ignored anyway if we are in rename and this is a not a named export.) - const importedName = symbolName(importedSymbol); + const importedName = symbolEscapedNameNoDefault(importedSymbol); if (importedName === undefined || importedName === InternalSymbolName.Default || importedName === symbol.escapedName) { return { kind: ImportExport.Import, symbol: importedSymbol, ...isImport }; } @@ -622,17 +622,6 @@ namespace ts.FindAllReferences { return isExternalModuleSymbol(exportingModuleSymbol) ? { exportingModuleSymbol, exportKind } : undefined; } - function symbolName(symbol: Symbol): __String | undefined { - if (symbol.escapedName !== InternalSymbolName.Default) { - return symbol.escapedName; - } - - return forEach(symbol.declarations, decl => { - const name = getNameOfDeclaration(decl); - return name && name.kind === SyntaxKind.Identifier && name.escapedText; - }); - } - /** If at an export specifier, go to the symbol it refers to. */ function skipExportSpecifierSymbol(symbol: Symbol, checker: TypeChecker): Symbol { // For `export { foo } from './bar", there's nothing to skip, because it does not create a new alias. But `export { foo } does. diff --git a/src/services/refactorProvider.ts b/src/services/refactorProvider.ts index f3504ed89b82d..adb3af3ea7adc 100644 --- a/src/services/refactorProvider.ts +++ b/src/services/refactorProvider.ts @@ -38,7 +38,7 @@ namespace ts { } } - export function getRefactorContextLength(context: RefactorContext): number { - return context.endPosition === undefined ? 0 : context.endPosition - context.startPosition; + export function getRefactorContextSpan({ startPosition, endPosition }: RefactorContext): TextSpan { + return createTextSpanFromBounds(startPosition, endPosition === undefined ? startPosition : endPosition); } } diff --git a/src/services/refactors/extractSymbol.ts b/src/services/refactors/extractSymbol.ts index 5805a9861801e..354c200217943 100644 --- a/src/services/refactors/extractSymbol.ts +++ b/src/services/refactors/extractSymbol.ts @@ -8,7 +8,7 @@ namespace ts.refactor.extractSymbol { * Exported for tests. */ export function getAvailableActions(context: RefactorContext): ApplicableRefactorInfo[] | undefined { - const rangeToExtract = getRangeToExtract(context.file, { start: context.startPosition, length: getRefactorContextLength(context) }); + const rangeToExtract = getRangeToExtract(context.file, getRefactorContextSpan(context)); const targetRange: TargetRange = rangeToExtract.targetRange; if (targetRange === undefined) { @@ -87,7 +87,7 @@ namespace ts.refactor.extractSymbol { /* Exported for tests */ export function getEditsForAction(context: RefactorContext, actionName: string): RefactorEditInfo | undefined { - const rangeToExtract = getRangeToExtract(context.file, { start: context.startPosition, length: getRefactorContextLength(context) }); + const rangeToExtract = getRangeToExtract(context.file, getRefactorContextSpan(context)); const targetRange: TargetRange = rangeToExtract.targetRange; const parsedFunctionIndexMatch = /^function_scope_(\d+)$/.exec(actionName); diff --git a/src/services/refactors/moveToNewFile.ts b/src/services/refactors/moveToNewFile.ts new file mode 100644 index 0000000000000..2f5a27f87f781 --- /dev/null +++ b/src/services/refactors/moveToNewFile.ts @@ -0,0 +1,628 @@ +/* @internal */ +namespace ts.refactor { + const refactorName = "Move to a new file"; + registerRefactor(refactorName, { + getAvailableActions(context): ApplicableRefactorInfo[] { + if (!context.preferences.allowTextChangesInNewFiles || getStatementsToMove(context) === undefined) return undefined; + const description = getLocaleSpecificMessage(Diagnostics.Move_to_a_new_file); + return [{ name: refactorName, description, actions: [{ name: refactorName, description }] }]; + }, + getEditsForAction(context, actionName): RefactorEditInfo { + Debug.assert(actionName === refactorName); + const statements = Debug.assertDefined(getStatementsToMove(context)); + const edits = textChanges.ChangeTracker.with(context, t => doChange(context.file, context.program, statements, t, context.host)); + return { edits, renameFilename: undefined, renameLocation: undefined }; + } + }); + + function getStatementsToMove(context: RefactorContext): ReadonlyArray | undefined { + const { file } = context; + const range = createTextRangeFromSpan(getRefactorContextSpan(context)); + const { statements } = file; + + const startNodeIndex = findIndex(statements, s => s.end > range.pos); + if (startNodeIndex === -1) return undefined; + // Can't only partially include the start node or be partially into the next node + if (range.pos > statements[startNodeIndex].getStart(file)) return undefined; + const afterEndNodeIndex = findIndex(statements, s => s.end > range.end, startNodeIndex); + // Can't be partially into the next node + if (afterEndNodeIndex !== -1 && (afterEndNodeIndex === 0 || statements[afterEndNodeIndex].getStart(file) < range.end)) return undefined; + + return statements.slice(startNodeIndex, afterEndNodeIndex === -1 ? statements.length : afterEndNodeIndex); + } + + function doChange(oldFile: SourceFile, program: Program, toMove: ReadonlyArray, changes: textChanges.ChangeTracker, host: LanguageServiceHost): void { + const checker = program.getTypeChecker(); + const usage = getUsageInfo(oldFile, toMove, checker); + + const currentDirectory = getDirectoryPath(oldFile.fileName); + const extension = extensionFromPath(oldFile.fileName); + const newModuleName = makeUniqueModuleName(getNewModuleName(usage.movedSymbols), extension, currentDirectory, host); + const newFileNameWithExtension = newModuleName + extension; + + // If previous file was global, this is easy. + changes.createNewFile(oldFile, combinePaths(currentDirectory, newFileNameWithExtension), getNewStatements(oldFile, usage, changes, toMove, program, newModuleName)); + + addNewFileToTsconfig(program, changes, oldFile.fileName, newFileNameWithExtension, hostGetCanonicalFileName(host)); + } + + function addNewFileToTsconfig(program: Program, changes: textChanges.ChangeTracker, oldFileName: string, newFileNameWithExtension: string, getCanonicalFileName: GetCanonicalFileName): void { + const cfg = program.getCompilerOptions().configFile; + if (!cfg) return; + + const newFileAbsolutePath = normalizePath(combinePaths(oldFileName, "..", newFileNameWithExtension)); + const newFilePath = getRelativePathFromFile(cfg.fileName, newFileAbsolutePath, getCanonicalFileName); + + const cfgObject = cfg.statements[0] && tryCast(cfg.statements[0].expression, isObjectLiteralExpression); + const filesProp = cfgObject && find(cfgObject.properties, (prop): prop is PropertyAssignment => + isPropertyAssignment(prop) && isStringLiteral(prop.name) && prop.name.text === "files"); + if (filesProp && isArrayLiteralExpression(filesProp.initializer)) { + changes.insertNodeInListAfter(cfg, last(filesProp.initializer.elements), createLiteral(newFilePath), filesProp.initializer.elements); + } + } + + function getNewStatements( + oldFile: SourceFile, usage: UsageInfo, changes: textChanges.ChangeTracker, toMove: ReadonlyArray, program: Program, newModuleName: string, + ): ReadonlyArray { + const checker = program.getTypeChecker(); + + if (!oldFile.externalModuleIndicator && !oldFile.commonJsModuleIndicator) { + changes.deleteNodeRange(oldFile, first(toMove), last(toMove)); + return toMove; + } + + const useEs6ModuleSyntax = !!oldFile.externalModuleIndicator; + const importsFromNewFile = createOldFileImportsFromNewFile(usage.oldFileImportsFromNewFile, newModuleName, useEs6ModuleSyntax); + if (importsFromNewFile) { + changes.insertNodeBefore(oldFile, oldFile.statements[0], importsFromNewFile, /*blankLineBetween*/ true); + } + + deleteUnusedOldImports(oldFile, toMove, changes, usage.unusedImportsFromOldFile, checker); + changes.deleteNodeRange(oldFile, first(toMove), last(toMove)); + + updateImportsInOtherFiles(changes, program, oldFile, usage.movedSymbols, newModuleName); + + return [ + ...getNewFileImportsAndAddExportInOldFile(oldFile, usage.oldImportsNeededByNewFile, usage.newFileImportsFromOldFile, changes, checker, useEs6ModuleSyntax), + ...addExports(oldFile, toMove, usage.oldFileImportsFromNewFile, useEs6ModuleSyntax), + ]; + } + + function deleteUnusedOldImports(oldFile: SourceFile, toMove: ReadonlyArray, changes: textChanges.ChangeTracker, toDelete: ReadonlySymbolSet, checker: TypeChecker) { + for (const statement of oldFile.statements) { + if (contains(toMove, statement)) continue; + forEachImportInStatement(statement, i => deleteUnusedImports(oldFile, i, changes, name => toDelete.has(checker.getSymbolAtLocation(name)))); + } + } + + function updateImportsInOtherFiles(changes: textChanges.ChangeTracker, program: Program, oldFile: SourceFile, movedSymbols: ReadonlySymbolSet, newModuleName: string): void { + const checker = program.getTypeChecker(); + for (const sourceFile of program.getSourceFiles()) { + if (sourceFile === oldFile) continue; + for (const statement of sourceFile.statements) { + forEachImportInStatement(statement, importNode => { + const shouldMove = (name: Identifier): boolean => { + const symbol = isBindingElement(name.parent) + ? getPropertySymbolFromBindingElement(checker, name.parent as BindingElement & { name: Identifier }) + : skipAlias(checker.getSymbolAtLocation(name), checker); + return !!symbol && movedSymbols.has(symbol); + }; + deleteUnusedImports(sourceFile, importNode, changes, shouldMove); // These will be changed to imports from the new file + const newModuleSpecifier = combinePaths(getDirectoryPath(moduleSpecifierFromImport(importNode).text), newModuleName); + const newImportDeclaration = filterImport(importNode, createLiteral(newModuleSpecifier), shouldMove); + if (newImportDeclaration) changes.insertNodeAfter(sourceFile, statement, newImportDeclaration); + }); + } + } + } + + function moduleSpecifierFromImport(i: SupportedImport): StringLiteralLike { + return (i.kind === SyntaxKind.ImportDeclaration ? i.moduleSpecifier + : i.kind === SyntaxKind.ImportEqualsDeclaration ? i.moduleReference.expression + : i.initializer.arguments[0]); + } + + function forEachImportInStatement(statement: Statement, cb: (importNode: SupportedImport) => void): void { + if (isImportDeclaration(statement)) { + if (isStringLiteral(statement.moduleSpecifier)) cb(statement as SupportedImport); + } + else if (isImportEqualsDeclaration(statement)) { + if (isExternalModuleReference(statement.moduleReference) && isStringLiteralLike(statement.moduleReference.expression)) { + cb(statement as SupportedImport); + } + } + else if (isVariableStatement(statement)) { + for (const decl of statement.declarationList.declarations) { + if (decl.initializer && isRequireCall(decl.initializer, /*checkArgumentIsStringLiteralLike*/ true)) { + cb(decl as SupportedImport); + } + } + } + } + + type SupportedImport = + | ImportDeclaration & { moduleSpecifier: StringLiteralLike } + | ImportEqualsDeclaration & { moduleReference: ExternalModuleReference & { expression: StringLiteralLike } } + | VariableDeclaration & { initializer: RequireOrImportCall }; + type SupportedImportStatement = + | ImportDeclaration + | ImportEqualsDeclaration + | VariableStatement; + + function createOldFileImportsFromNewFile(newFileNeedExport: ReadonlySymbolSet, newFileNameWithExtension: string, useEs6Imports: boolean): Statement | undefined { + let defaultImport: Identifier | undefined; + const imports: string[] = []; + newFileNeedExport.forEach(symbol => { + if (symbol.escapedName === InternalSymbolName.Default) { + defaultImport = createIdentifier(symbolNameNoDefault(symbol)); + } + else { + imports.push(symbol.name); + } + }); + return makeImportOrRequire(defaultImport, imports, newFileNameWithExtension, useEs6Imports); + } + + function makeImportOrRequire(defaultImport: Identifier | undefined, imports: ReadonlyArray, path: string, useEs6Imports: boolean): Statement | undefined { + path = ensurePathIsNonModuleName(path); + if (useEs6Imports) { + const specifiers = imports.map(i => createImportSpecifier(/*propertyName*/ undefined, createIdentifier(i))); + return makeImportIfNecessary(defaultImport, specifiers, path); + } + else { + Debug.assert(!defaultImport); // If there's a default export, it should have been an es6 module. + const bindingElements = imports.map(i => createBindingElement(/*dotDotDotToken*/ undefined, /*propertyName*/ undefined, i)); + return bindingElements.length + ? makeVariableStatement(createObjectBindingPattern(bindingElements), /*type*/ undefined, createRequireCall(createLiteral(path))) + : undefined; + } + } + + function makeVariableStatement(name: BindingName, type: TypeNode | undefined, initializer: Expression | undefined, flags: NodeFlags = NodeFlags.Const) { + return createVariableStatement(/*modifiers*/ undefined, createVariableDeclarationList([createVariableDeclaration(name, type, initializer)], flags)); + } + + function createRequireCall(moduleSpecifier: StringLiteralLike): CallExpression { + return createCall(createIdentifier("require"), /*typeArguments*/ undefined, [moduleSpecifier]); + } + + function addExports(sourceFile: SourceFile, toMove: ReadonlyArray, needExport: ReadonlySymbolSet, useEs6Exports: boolean): ReadonlyArray { + return flatMap(toMove, statement => { + if (isTopLevelDeclarationStatement(statement) && + !isExported(sourceFile, statement, useEs6Exports) && + forEachTopLevelDeclaration(statement, d => needExport.has(Debug.assertDefined(d.symbol)))) { + const exports = addExport(statement, useEs6Exports); + if (exports) return exports; + } + return statement; + }); + } + + function deleteUnusedImports(sourceFile: SourceFile, importDecl: SupportedImport, changes: textChanges.ChangeTracker, isUnused: (name: Identifier) => boolean): void { + switch (importDecl.kind) { + case SyntaxKind.ImportDeclaration: + deleteUnusedImportsInDeclaration(sourceFile, importDecl, changes, isUnused); + break; + case SyntaxKind.ImportEqualsDeclaration: + if (isUnused(importDecl.name)) { + changes.deleteNode(sourceFile, importDecl); + } + break; + case SyntaxKind.VariableDeclaration: + deleteUnusedImportsInVariableDeclaration(sourceFile, importDecl, changes, isUnused); + break; + default: + Debug.assertNever(importDecl); + } + } + function deleteUnusedImportsInDeclaration(sourceFile: SourceFile, importDecl: ImportDeclaration, changes: textChanges.ChangeTracker, isUnused: (name: Identifier) => boolean): void { + if (!importDecl.importClause) return; + const { name, namedBindings } = importDecl.importClause; + const defaultUnused = !name || isUnused(name); + const namedBindingsUnused = !namedBindings || + (namedBindings.kind === SyntaxKind.NamespaceImport ? isUnused(namedBindings.name) : namedBindings.elements.every(e => isUnused(e.name))); + if (defaultUnused && namedBindingsUnused) { + changes.deleteNode(sourceFile, importDecl); + } + else { + if (name && defaultUnused) { + changes.deleteNode(sourceFile, name); + } + if (namedBindings) { + if (namedBindingsUnused) { + changes.deleteNode(sourceFile, namedBindings); + } + else if (namedBindings.kind === SyntaxKind.NamedImports) { + for (const element of namedBindings.elements) { + if (isUnused(element.name)) changes.deleteNodeInList(sourceFile, element); + } + } + } + } + } + function deleteUnusedImportsInVariableDeclaration(sourceFile: SourceFile, varDecl: VariableDeclaration, changes: textChanges.ChangeTracker, isUnused: (name: Identifier) => boolean) { + const { name } = varDecl; + switch (name.kind) { + case SyntaxKind.Identifier: + if (isUnused(name)) { + changes.deleteNode(sourceFile, name); + } + break; + case SyntaxKind.ArrayBindingPattern: + break; + case SyntaxKind.ObjectBindingPattern: + if (name.elements.every(e => isIdentifier(e.name) && isUnused(e.name))) { + changes.deleteNode(sourceFile, + isVariableDeclarationList(varDecl.parent) && varDecl.parent.declarations.length === 1 ? varDecl.parent.parent : varDecl); + } + else { + for (const element of name.elements) { + if (isIdentifier(element.name) && isUnused(element.name)) { + changes.deleteNode(sourceFile, element.name); + } + } + } + break; + } + } + + function getNewFileImportsAndAddExportInOldFile( + oldFile: SourceFile, + importsToCopy: ReadonlySymbolSet, + newFileImportsFromOldFile: ReadonlySymbolSet, + changes: textChanges.ChangeTracker, + checker: TypeChecker, + useEs6ModuleSyntax: boolean, + ): ReadonlyArray { + const copiedOldImports: SupportedImportStatement[] = []; + for (const oldStatement of oldFile.statements) { + forEachImportInStatement(oldStatement, i => { + append(copiedOldImports, filterImport(i, moduleSpecifierFromImport(i), name => importsToCopy.has(checker.getSymbolAtLocation(name)))); + }); + } + + // Also, import things used from the old file, and insert 'export' modifiers as necessary in the old file. + let oldFileDefault: Identifier | undefined; + const oldFileNamedImports: string[] = []; + const markSeenTop = nodeSeenTracker(); // Needed because multiple declarations may appear in `const x = 0, y = 1;`. + newFileImportsFromOldFile.forEach(symbol => { + for (const decl of symbol.declarations) { + if (!isTopLevelDeclaration(decl)) continue; + const name = nameOfTopLevelDeclaration(decl); + if (!name) continue; + + const top = getTopLevelDeclarationStatement(decl); + if (markSeenTop(top)) { + addExportToChanges(oldFile, top, changes, useEs6ModuleSyntax); + } + if (hasModifier(decl, ModifierFlags.Default)) { + oldFileDefault = name; + } + else { + oldFileNamedImports.push(name.text); + } + } + }); + + append(copiedOldImports, makeImportOrRequire(oldFileDefault, oldFileNamedImports, removeFileExtension(getBaseFileName(oldFile.fileName)), useEs6ModuleSyntax)); + return copiedOldImports; + } + + function makeUniqueModuleName(moduleName: string, extension: string, inDirectory: string, host: LanguageServiceHost): string { + let newModuleName = moduleName; + for (let i = 1; ; i++) { + const name = combinePaths(inDirectory, newModuleName + extension); + if (!host.fileExists(name)) return newModuleName; + newModuleName = `${moduleName}.${i}`; + } + } + + function getNewModuleName(movedSymbols: ReadonlySymbolSet): string { + return movedSymbols.forEachEntry(symbolNameNoDefault) || "newFile"; + } + + interface UsageInfo { + // Symbols whose declarations are moved from the old file to the new file. + readonly movedSymbols: ReadonlySymbolSet; + + // Symbols declared in the old file that must be imported by the new file. (May not already be exported.) + readonly newFileImportsFromOldFile: ReadonlySymbolSet; + // Subset of movedSymbols that are still used elsewhere in the old file and must be imported back. + readonly oldFileImportsFromNewFile: ReadonlySymbolSet; + + readonly oldImportsNeededByNewFile: ReadonlySymbolSet; + // Subset of oldImportsNeededByNewFile that are will no longer be used in the old file. + readonly unusedImportsFromOldFile: ReadonlySymbolSet; + } + function getUsageInfo(oldFile: SourceFile, toMove: ReadonlyArray, checker: TypeChecker): UsageInfo { + const movedSymbols = new SymbolSet(); + const oldImportsNeededByNewFile = new SymbolSet(); + const newFileImportsFromOldFile = new SymbolSet(); + + for (const statement of toMove) { + forEachTopLevelDeclaration(statement, decl => { + movedSymbols.add(Debug.assertDefined(isExpressionStatement(decl) ? checker.getSymbolAtLocation(decl.expression.left) : decl.symbol)); + }); + } + for (const statement of toMove) { + forEachReference(statement, checker, symbol => { + if (!symbol.declarations) return; + for (const decl of symbol.declarations) { + if (isInImport(decl)) { + oldImportsNeededByNewFile.add(symbol); + } + else if (isTopLevelDeclaration(decl) && !movedSymbols.has(symbol)) { + newFileImportsFromOldFile.add(symbol); + } + } + }); + } + + const unusedImportsFromOldFile = oldImportsNeededByNewFile.clone(); + + const oldFileImportsFromNewFile = new SymbolSet(); + for (const statement of oldFile.statements) { + if (contains(toMove, statement)) continue; + + forEachReference(statement, checker, symbol => { + if (movedSymbols.has(symbol)) oldFileImportsFromNewFile.add(symbol); + unusedImportsFromOldFile.delete(symbol); + }); + } + + return { movedSymbols, newFileImportsFromOldFile, oldFileImportsFromNewFile, oldImportsNeededByNewFile, unusedImportsFromOldFile }; + } + + // Below should all be utilities + + function isInImport(decl: Declaration) { + switch (decl.kind) { + case SyntaxKind.ImportEqualsDeclaration: + case SyntaxKind.ImportSpecifier: + case SyntaxKind.ImportClause: + return true; + case SyntaxKind.VariableDeclaration: + return isVariableDeclarationInImport(decl as VariableDeclaration); + case SyntaxKind.BindingElement: + return isVariableDeclaration(decl.parent.parent) && isVariableDeclarationInImport(decl.parent.parent); + default: + return false; + } + } + function isVariableDeclarationInImport(decl: VariableDeclaration) { + return isSourceFile(decl.parent.parent.parent) && + isRequireCall(decl.initializer, /*checkArgumentIsStringLiteralLike*/ true); + } + + function filterImport(i: SupportedImport, moduleSpecifier: StringLiteralLike, keep: (name: Identifier) => boolean): SupportedImportStatement | undefined { + switch (i.kind) { + case SyntaxKind.ImportDeclaration: { + const clause = i.importClause; + const defaultImport = clause.name && keep(clause.name) ? clause.name : undefined; + const namedBindings = clause.namedBindings && filterNamedBindings(clause.namedBindings, keep); + return defaultImport || namedBindings + ? createImportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, createImportClause(defaultImport, namedBindings), moduleSpecifier) + : undefined; + } + case SyntaxKind.ImportEqualsDeclaration: + return keep(i.name) ? i : undefined; + case SyntaxKind.VariableDeclaration: { + const name = filterBindingName(i.name, keep); + return name ? makeVariableStatement(name, i.type, createRequireCall(moduleSpecifier), i.parent.flags) : undefined; + } + default: + return Debug.assertNever(i); + } + } + function filterNamedBindings(namedBindings: NamedImportBindings, keep: (name: Identifier) => boolean): NamedImportBindings | undefined { + if (namedBindings.kind === SyntaxKind.NamespaceImport) { + return keep(namedBindings.name) ? namedBindings : undefined; + } + else { + const newElements = namedBindings.elements.filter(e => keep(e.name)); + return newElements.length ? createNamedImports(newElements) : undefined; + } + } + function filterBindingName(name: BindingName, keep: (name: Identifier) => boolean): BindingName | undefined { + switch (name.kind) { + case SyntaxKind.Identifier: + return keep(name) ? name : undefined; + case SyntaxKind.ArrayBindingPattern: + return name; + case SyntaxKind.ObjectBindingPattern: { + // We can't handle nested destructurings or property names well here, so just copy them all. + const newElements = name.elements.filter(prop => prop.propertyName || !isIdentifier(prop.name) || keep(prop.name)); + return newElements.length ? createObjectBindingPattern(newElements) : undefined; + } + } + } + + function forEachReference(node: Node, checker: TypeChecker, onReference: (s: Symbol) => void) { + node.forEachChild(function cb(node) { + if (isIdentifier(node) && !isDeclarationName(node)) { + const sym = checker.getSymbolAtLocation(node); + if (sym) onReference(sym); + } + else { + node.forEachChild(cb); + } + }); + } + + interface ReadonlySymbolSet { + has(symbol: Symbol): boolean; + forEach(cb: (symbol: Symbol) => void): void; + forEachEntry(cb: (symbol: Symbol) => T | undefined): T | undefined; + } + class SymbolSet implements ReadonlySymbolSet { + private map = createMap(); + add(symbol: Symbol): void { + this.map.set(String(getSymbolId(symbol)), symbol); + } + has(symbol: Symbol): boolean { + return this.map.has(String(getSymbolId(symbol))); + } + delete(symbol: Symbol): void { + this.map.delete(String(getSymbolId(symbol))); + } + forEach(cb: (symbol: Symbol) => void): void { + this.map.forEach(cb); + } + forEachEntry(cb: (symbol: Symbol) => T | undefined): T | undefined { + return forEachEntry(this.map, cb); + } + clone(): SymbolSet { + const clone = new SymbolSet(); + copyEntries(this.map, clone.map); + return clone; + } + } + + type TopLevelExpressionStatement = ExpressionStatement & { expression: BinaryExpression & { left: PropertyAccessExpression } }; // 'exports.x = ...' + type NonVariableTopLevelDeclaration = + | FunctionDeclaration + | ClassDeclaration + | EnumDeclaration + | TypeAliasDeclaration + | InterfaceDeclaration + | ModuleDeclaration + | TopLevelExpressionStatement + | ImportEqualsDeclaration; + type TopLevelDeclarationStatement = NonVariableTopLevelDeclaration | VariableStatement; + interface TopLevelVariableDeclaration extends VariableDeclaration { parent: VariableDeclarationList & { parent: VariableStatement; }; } + type TopLevelDeclaration = NonVariableTopLevelDeclaration | TopLevelVariableDeclaration; + function isTopLevelDeclaration(node: Node): node is TopLevelDeclaration { + return isNonVariableTopLevelDeclaration(node) || isVariableDeclaration(node) && isSourceFile(node.parent.parent.parent); + } + + function isTopLevelDeclarationStatement(node: Node): node is TopLevelDeclarationStatement { + Debug.assert(isSourceFile(node.parent)); + return isNonVariableTopLevelDeclaration(node) || isVariableStatement(node); + } + + function isNonVariableTopLevelDeclaration(node: Node): node is NonVariableTopLevelDeclaration { + switch (node.kind) { + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.ClassDeclaration: + case SyntaxKind.ModuleDeclaration: + case SyntaxKind.EnumDeclaration: + case SyntaxKind.TypeAliasDeclaration: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.ImportEqualsDeclaration: + return true; + default: + return false; + } + } + + function forEachTopLevelDeclaration(statement: Statement, cb: (node: TopLevelDeclaration) => T): T { + switch (statement.kind) { + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.ClassDeclaration: + case SyntaxKind.ModuleDeclaration: + case SyntaxKind.EnumDeclaration: + case SyntaxKind.TypeAliasDeclaration: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.ImportEqualsDeclaration: + return cb(statement as FunctionDeclaration | ClassDeclaration | EnumDeclaration | ModuleDeclaration | TypeAliasDeclaration | InterfaceDeclaration | ImportEqualsDeclaration); + + case SyntaxKind.VariableStatement: + return forEach((statement as VariableStatement).declarationList.declarations as ReadonlyArray, cb); + + case SyntaxKind.ExpressionStatement: { + const { expression } = statement as ExpressionStatement; + return isBinaryExpression(expression) && getSpecialPropertyAssignmentKind(expression) === SpecialPropertyAssignmentKind.ExportsProperty + ? cb(statement as TopLevelExpressionStatement) + : undefined; + } + } + } + + function nameOfTopLevelDeclaration(d: TopLevelDeclaration): Identifier | undefined { + return d.kind === SyntaxKind.ExpressionStatement ? d.expression.left.name : tryCast(d.name, isIdentifier); + } + + function getTopLevelDeclarationStatement(d: TopLevelDeclaration): TopLevelDeclarationStatement { + return isVariableDeclaration(d) ? d.parent.parent : d; + } + + function addExportToChanges(sourceFile: SourceFile, decl: TopLevelDeclarationStatement, changes: textChanges.ChangeTracker, useEs6Exports: boolean): void { + if (isExported(sourceFile, decl, useEs6Exports)) return; + if (useEs6Exports) { + if (!isExpressionStatement(decl)) changes.insertExportModifier(sourceFile, decl); + } + else { + const names = getNamesToExportInCommonJS(decl); + if (names.length !== 0) changes.insertNodesAfter(sourceFile, decl, names.map(createExportAssignment)); + } + } + + function isExported(sourceFile: SourceFile, decl: TopLevelDeclarationStatement, useEs6Exports: boolean): boolean { + if (useEs6Exports) { + return !isExpressionStatement(decl) && hasModifier(decl, ModifierFlags.Export); + } + else { + return getNamesToExportInCommonJS(decl).some(name => sourceFile.symbol.exports.has(escapeLeadingUnderscores(name))); + } + } + + function addExport(decl: TopLevelDeclarationStatement, useEs6Exports: boolean): ReadonlyArray | undefined { + return useEs6Exports ? [addEs6Export(decl)] : addCommonjsExport(decl); + } + function addEs6Export(d: TopLevelDeclarationStatement): TopLevelDeclarationStatement { + const modifiers = concatenate([createModifier(SyntaxKind.ExportKeyword)], d.modifiers); + switch (d.kind) { + case SyntaxKind.FunctionDeclaration: + return updateFunctionDeclaration(d, d.decorators, modifiers, d.asteriskToken, d.name, d.typeParameters, d.parameters, d.type, d.body); + case SyntaxKind.ClassDeclaration: + return updateClassDeclaration(d, d.decorators, modifiers, d.name, d.typeParameters, d.heritageClauses, d.members); + case SyntaxKind.VariableStatement: + return updateVariableStatement(d, modifiers, d.declarationList); + case SyntaxKind.ModuleDeclaration: + return updateModuleDeclaration(d, d.decorators, modifiers, d.name, d.body); + case SyntaxKind.EnumDeclaration: + return updateEnumDeclaration(d, d.decorators, modifiers, d.name, d.members); + case SyntaxKind.TypeAliasDeclaration: + return updateTypeAliasDeclaration(d, d.decorators, modifiers, d.name, d.typeParameters, d.type); + case SyntaxKind.InterfaceDeclaration: + return updateInterfaceDeclaration(d, d.decorators, modifiers, d.name, d.typeParameters, d.heritageClauses, d.members); + case SyntaxKind.ImportEqualsDeclaration: + return updateImportEqualsDeclaration(d, d.decorators, modifiers, d.name, d.moduleReference); + case SyntaxKind.ExpressionStatement: + return Debug.fail(); // Shouldn't try to add 'export' keyword to `exports.x = ...` + default: + return Debug.assertNever(d); + } + } + function addCommonjsExport(decl: TopLevelDeclarationStatement): ReadonlyArray | undefined { + return [decl, ...getNamesToExportInCommonJS(decl).map(createExportAssignment)]; + } + function getNamesToExportInCommonJS(decl: TopLevelDeclarationStatement): ReadonlyArray { + switch (decl.kind) { + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.ClassDeclaration: + return [decl.name.text]; + case SyntaxKind.VariableStatement: + return mapDefined(decl.declarationList.declarations, d => isIdentifier(d.name) ? d.name.text : undefined); + case SyntaxKind.ModuleDeclaration: + case SyntaxKind.EnumDeclaration: + case SyntaxKind.TypeAliasDeclaration: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.ImportEqualsDeclaration: + return undefined; + case SyntaxKind.ExpressionStatement: + return Debug.fail(); // Shouldn't try to add 'export' keyword to `exports.x = ...` + default: + Debug.assertNever(decl); + } + } + + /** Creates `exports.x = x;` */ + function createExportAssignment(name: string): Statement { + return createExpressionStatement( + createBinary( + createPropertyAccess(createIdentifier("exports"), createIdentifier(name)), + SyntaxKind.EqualsToken, + createIdentifier(name))); + } +} diff --git a/src/services/textChanges.ts b/src/services/textChanges.ts index eca1c639196e9..de588120db124 100644 --- a/src/services/textChanges.ts +++ b/src/services/textChanges.ts @@ -211,6 +211,7 @@ namespace ts.textChanges { export class ChangeTracker { private readonly changes: Change[] = []; + private readonly newFiles: { readonly oldFile: SourceFile, readonly fileName: string, readonly statements: ReadonlyArray }[] = []; private readonly deletedNodesInLists: true[] = []; // Stores ids of nodes in lists that we already deleted. Used to avoid deleting `, ` twice in `a, b`. private readonly classesWithNodesInsertedAtStart = createMap(); // Set implemented as Map @@ -463,7 +464,17 @@ namespace ts.textChanges { } } - public insertNodeAfter(sourceFile: SourceFile, after: Node, newNode: Node): this { + public insertNodeAfter(sourceFile: SourceFile, after: Node, newNode: Node): void { + const endPosition = this.insertNodeAfterWorker(sourceFile, after, newNode); + this.insertNodeAt(sourceFile, endPosition, newNode, this.getInsertNodeAfterOptions(sourceFile, after)); + } + + public insertNodesAfter(sourceFile: SourceFile, after: Node, newNodes: ReadonlyArray): void { + const endPosition = this.insertNodeAfterWorker(sourceFile, after, first(newNodes)); + this.insertNodesAt(sourceFile, endPosition, newNodes, this.getInsertNodeAfterOptions(sourceFile, after)); + } + + private insertNodeAfterWorker(sourceFile: SourceFile, after: Node, newNode: Node): number { if (needSemicolonBetween(after, newNode)) { // check if previous statement ends with semicolon // if not - insert semicolon to preserve the code from changing the meaning due to ASI @@ -472,14 +483,17 @@ namespace ts.textChanges { } } const endPosition = getAdjustedEndPosition(sourceFile, after, {}); - const options = this.getInsertNodeAfterOptions(after); - return this.replaceRange(sourceFile, createTextRange(endPosition), newNode, { + return endPosition; + } + + private getInsertNodeAfterOptions(sourceFile: SourceFile, after: Node) { + const options = this.getInsertNodeAfterOptionsWorker(after); + return { ...options, prefix: after.end === sourceFile.end && isStatement(after) ? (options.prefix ? `\n${options.prefix}` : "\n") : options.prefix, - }); + }; } - - private getInsertNodeAfterOptions(node: Node): InsertNodeOptions { + private getInsertNodeAfterOptionsWorker(node: Node): InsertNodeOptions { if (isClassDeclaration(node) || isModuleDeclaration(node)) { return { prefix: this.newLineCharacter, suffix: this.newLineCharacter }; } @@ -527,13 +541,16 @@ namespace ts.textChanges { } } + public insertExportModifier(sourceFile: SourceFile, node: DeclarationStatement | VariableStatement): void { + this.insertText(sourceFile, node.getStart(sourceFile), "export "); + } + /** * This function should be used to insert nodes in lists when nodes don't carry separators as the part of the node range, * i.e. arguments in arguments lists, parameters in parameter lists etc. * Note that separators are part of the node in statements and class elements. */ - public insertNodeInListAfter(sourceFile: SourceFile, after: Node, newNode: Node) { - const containingList = formatting.SmartIndenter.getContainingList(after, sourceFile); + public insertNodeInListAfter(sourceFile: SourceFile, after: Node, newNode: Node, containingList = formatting.SmartIndenter.getContainingList(after, sourceFile)) { if (!containingList) { Debug.fail("node is not a list element"); return this; @@ -664,7 +681,15 @@ namespace ts.textChanges { */ public getChanges(validate?: ValidateNonFormattedText): FileTextChanges[] { this.finishClassesWithNodesInsertedAtStart(); - return changesToText.getTextChangesFromChanges(this.changes, this.newLineCharacter, this.formatContext, validate); + const changes = changesToText.getTextChangesFromChanges(this.changes, this.newLineCharacter, this.formatContext, validate); + for (const { oldFile, fileName, statements } of this.newFiles) { + changes.push(changesToText.newFileChanges(oldFile, fileName, statements, this.newLineCharacter)); + } + return changes; + } + + public createNewFile(oldFile: SourceFile, fileName: string, statements: ReadonlyArray) { + this.newFiles.push({ oldFile, fileName, statements }); } } @@ -679,7 +704,8 @@ namespace ts.textChanges { return group(changes, c => c.sourceFile.path).map(changesInFile => { const sourceFile = changesInFile[0].sourceFile; // order changes by start position - const normalized = stableSort(changesInFile, (a, b) => a.range.pos - b.range.pos); + // If the start position is the same, put the shorter range first, since an empty range (x, x) may precede (x, y) but not vice-versa. + const normalized = stableSort(changesInFile, (a, b) => (a.range.pos - b.range.pos) || (a.range.end - b.range.end)); // verify that change intervals do not overlap, except possibly at end points. for (let i = 0; i < normalized.length - 1; i++) { Debug.assert(normalized[i].range.end <= normalized[i + 1].range.pos, "Changes overlap", () => @@ -691,6 +717,11 @@ namespace ts.textChanges { }); } + export function newFileChanges(oldFile: SourceFile, fileName: string, statements: ReadonlyArray, newLineCharacter: string): FileTextChanges { + const text = statements.map(s => getNonformattedText(s, oldFile, newLineCharacter).text).join(newLineCharacter); + return { fileName, textChanges: [createTextChange(createTextSpan(0, 0), text)] }; + } + function computeNewText(change: Change, sourceFile: SourceFile, newLineCharacter: string, formatContext: formatting.FormatContext, validate: ValidateNonFormattedText): string { if (change.kind === ChangeKind.Remove) { return ""; diff --git a/src/services/tsconfig.json b/src/services/tsconfig.json index be46c21e06b12..c38a7324b30cd 100644 --- a/src/services/tsconfig.json +++ b/src/services/tsconfig.json @@ -109,6 +109,7 @@ "codefixes/useDefaultImport.ts", "refactors/extractSymbol.ts", "refactors/generateGetAccessorAndSetAccessor.ts", + "refactors/moveToNewFile.ts", "sourcemaps.ts", "services.ts", "breakpoints.ts", diff --git a/src/services/types.ts b/src/services/types.ts index c103d2f4ba046..cf2e58a3712a6 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -13,7 +13,7 @@ namespace ts { getStart(sourceFile?: SourceFileLike, includeJsDocComment?: boolean): number; getFullStart(): number; getEnd(): number; - getWidth(sourceFile?: SourceFile): number; + getWidth(sourceFile?: SourceFileLike): number; getFullWidth(): number; getLeadingTriviaWidth(sourceFile?: SourceFile): number; getFullText(sourceFile?: SourceFile): string; @@ -234,6 +234,7 @@ namespace ts { readonly includeCompletionsForModuleExports?: boolean; readonly includeCompletionsWithInsertText?: boolean; readonly importModuleSpecifierPreference?: "relative" | "non-relative"; + readonly allowTextChangesInNewFiles?: boolean; } /* @internal */ export const defaultPreferences: UserPreferences = {}; diff --git a/src/services/utilities.ts b/src/services/utilities.ts index d5694c936deee..55b5f4f0ff910 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -707,7 +707,7 @@ namespace ts { return findPrecedingToken(position, file); } - export function findNextToken(previousToken: Node, parent: Node): Node { + export function findNextToken(previousToken: Node, parent: Node, sourceFile: SourceFile): Node { return find(parent); function find(n: Node): Node { @@ -724,7 +724,7 @@ namespace ts { // previous token ends exactly at the beginning of child (child.pos === previousToken.end); - if (shouldDiveInChildNode && nodeHasTokens(child)) { + if (shouldDiveInChildNode && nodeHasTokens(child, sourceFile)) { return find(child); } } @@ -759,12 +759,12 @@ namespace ts { const start = child.getStart(sourceFile, includeJsDoc); const lookInPreviousChild = (start >= position) || // cursor in the leading trivia - !nodeHasTokens(child) || + !nodeHasTokens(child, sourceFile) || isWhiteSpaceOnlyJsxText(child); if (lookInPreviousChild) { // actual start of the node is past the position - previous token should be at the end of previous child - const candidate = findRightmostChildNodeWithTokens(children, /*exclusiveStartPosition*/ i); + const candidate = findRightmostChildNodeWithTokens(children, /*exclusiveStartPosition*/ i, sourceFile); return candidate && findRightmostToken(candidate, sourceFile); } else { @@ -781,7 +781,7 @@ namespace ts { // Try to find the rightmost token in the file without filtering. // Namely we are skipping the check: 'position < node.end' if (children.length) { - const candidate = findRightmostChildNodeWithTokens(children, /*exclusiveStartPosition*/ children.length); + const candidate = findRightmostChildNodeWithTokens(children, /*exclusiveStartPosition*/ children.length, sourceFile); return candidate && findRightmostToken(candidate, sourceFile); } } @@ -797,21 +797,21 @@ namespace ts { } const children = n.getChildren(sourceFile); - const candidate = findRightmostChildNodeWithTokens(children, /*exclusiveStartPosition*/ children.length); + const candidate = findRightmostChildNodeWithTokens(children, /*exclusiveStartPosition*/ children.length, sourceFile); return candidate && findRightmostToken(candidate, sourceFile); } /** * Finds the rightmost child to the left of `children[exclusiveStartPosition]` which is a non-all-whitespace token or has constituent tokens. */ - function findRightmostChildNodeWithTokens(children: Node[], exclusiveStartPosition: number): Node | undefined { + function findRightmostChildNodeWithTokens(children: Node[], exclusiveStartPosition: number, sourceFile: SourceFile): Node | undefined { for (let i = exclusiveStartPosition - 1; i >= 0; i--) { const child = children[i]; if (isWhiteSpaceOnlyJsxText(child)) { Debug.assert(i > 0, "`JsxText` tokens should not be the first child of `JsxElement | JsxSelfClosingElement`"); } - else if (nodeHasTokens(children[i])) { + else if (nodeHasTokens(children[i], sourceFile)) { return children[i]; } } @@ -1022,10 +1022,10 @@ namespace ts { } } - function nodeHasTokens(n: Node): boolean { + function nodeHasTokens(n: Node, sourceFile: SourceFileLike): boolean { // If we have a token or node that has a non-zero width, it must have tokens. // Note: getWidth() does not take trivia into account. - return n.getWidth() !== 0; + return n.getWidth(sourceFile) !== 0; } export function getNodeModifiers(node: Node): string { @@ -1146,6 +1146,10 @@ namespace ts { return createTextSpanFromBounds(range.pos, range.end); } + export function createTextRangeFromSpan(span: TextSpan): TextRange { + return createTextRange(span.start, span.start + span.length); + } + export function createTextChangeFromStartLength(start: number, length: number, newText: string): TextChange { return createTextChange(createTextSpan(start, length), newText); } @@ -1225,6 +1229,47 @@ namespace ts { export function hostGetCanonicalFileName(host: LanguageServiceHost): GetCanonicalFileName { return createGetCanonicalFileName(hostUsesCaseSensitiveFileNames(host)); } + + export function makeImportIfNecessary(defaultImport: Identifier | undefined, namedImports: ReadonlyArray | undefined, moduleSpecifier: string): ImportDeclaration | undefined { + return defaultImport || namedImports && namedImports.length ? makeImport(defaultImport, namedImports, moduleSpecifier) : undefined; + } + + export function makeImport(defaultImport: Identifier | undefined, namedImports: ReadonlyArray | undefined, moduleSpecifier: string | Expression): ImportDeclaration { + return createImportDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, + defaultImport || namedImports + ? createImportClause(defaultImport, namedImports && namedImports.length ? createNamedImports(namedImports) : undefined) + : undefined, + typeof moduleSpecifier === "string" ? createLiteral(moduleSpecifier) : moduleSpecifier); + } + + export function symbolNameNoDefault(symbol: Symbol): string | undefined { + const escaped = symbolEscapedNameNoDefault(symbol); + return escaped === undefined ? undefined : unescapeLeadingUnderscores(escaped); + } + + export function symbolEscapedNameNoDefault(symbol: Symbol): __String | undefined { + if (symbol.escapedName !== InternalSymbolName.Default) { + return symbol.escapedName; + } + + return firstDefined(symbol.declarations, decl => { + const name = getNameOfDeclaration(decl); + return name && name.kind === SyntaxKind.Identifier ? name.escapedText : undefined; + }); + } + + export function getPropertySymbolFromBindingElement(checker: TypeChecker, bindingElement: BindingElement & { name: Identifier }) { + const typeOfPattern = checker.getTypeAtLocation(bindingElement.parent); + const propSymbol = typeOfPattern && checker.getPropertyOfType(typeOfPattern, bindingElement.name.text); + if (propSymbol && propSymbol.flags & SymbolFlags.Accessor) { + // See GH#16922 + Debug.assert(!!(propSymbol.flags & SymbolFlags.Transient)); + return (propSymbol as TransientSymbol).target; + } + return propSymbol; + } } // Display-part writer helpers diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 6f62203f0b765..8cccebc9ffbc8 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -4326,7 +4326,7 @@ declare namespace ts { getStart(sourceFile?: SourceFile, includeJsDocComment?: boolean): number; getFullStart(): number; getEnd(): number; - getWidth(sourceFile?: SourceFile): number; + getWidth(sourceFile?: SourceFileLike): number; getFullWidth(): number; getLeadingTriviaWidth(sourceFile?: SourceFile): number; getFullText(sourceFile?: SourceFile): string; @@ -4468,6 +4468,7 @@ declare namespace ts { readonly includeCompletionsForModuleExports?: boolean; readonly includeCompletionsWithInsertText?: boolean; readonly importModuleSpecifierPreference?: "relative" | "non-relative"; + readonly allowTextChangesInNewFiles?: boolean; } interface LanguageService { cleanupSemanticCache(): void; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index d154440d0d9c4..a760b1816ae97 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -4326,7 +4326,7 @@ declare namespace ts { getStart(sourceFile?: SourceFile, includeJsDocComment?: boolean): number; getFullStart(): number; getEnd(): number; - getWidth(sourceFile?: SourceFile): number; + getWidth(sourceFile?: SourceFileLike): number; getFullWidth(): number; getLeadingTriviaWidth(sourceFile?: SourceFile): number; getFullText(sourceFile?: SourceFile): string; @@ -4468,6 +4468,7 @@ declare namespace ts { readonly includeCompletionsForModuleExports?: boolean; readonly includeCompletionsWithInsertText?: boolean; readonly importModuleSpecifierPreference?: "relative" | "non-relative"; + readonly allowTextChangesInNewFiles?: boolean; } interface LanguageService { cleanupSemanticCache(): void; diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index 8a7aae00c2429..4c5134e02440c 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -347,7 +347,11 @@ declare namespace FourSlashInterface { oldPath: string; newPath: string; newFileContents: { [fileName: string]: string }; - }); + }): void; + moveToNewFile(options: { + readonly newFileContents: { readonly [fileName: string]: string }; + }): void; + noMoveToNewFile(): void; } class edit { backspace(count?: number): void; diff --git a/tests/cases/fourslash/moveToNewFile.ts b/tests/cases/fourslash/moveToNewFile.ts new file mode 100644 index 0000000000000..cd2bd4ff8d62a --- /dev/null +++ b/tests/cases/fourslash/moveToNewFile.ts @@ -0,0 +1,23 @@ +/// + +// @Filename: /a.ts +////import { a, b, alreadyUnused } from "./other"; +////const p = 0; +////[|const y = p + b;|] +////a; y; + +verify.moveToNewFile({ + newFileContents: { + "/a.ts": +`import { y } from "./y"; + +import { a, alreadyUnused } from "./other"; +export const p = 0; +a; y;`, + + "/y.ts": +`import { b } from "./other"; +import { p } from "./a"; +export const y = p + b;`, + }, +}); diff --git a/tests/cases/fourslash/moveToNewFile_declarationKinds.ts b/tests/cases/fourslash/moveToNewFile_declarationKinds.ts new file mode 100644 index 0000000000000..6420a9e8ce153 --- /dev/null +++ b/tests/cases/fourslash/moveToNewFile_declarationKinds.ts @@ -0,0 +1,38 @@ +/// + +// @Filename: /a.ts +////export {}; // make this a module +////[|const x = 0; +////function f() {} +////class C {} +////enum E {} +////namespace N { export const x = 0; } +////type T = number; +////interface I {}|] +////x; f; C; E; N; +////type U = T; type V = I; + +verify.moveToNewFile({ + newFileContents: { + "/a.ts": +`import { x, f, C, E, N, T, I } from "./x"; + +export {}; // make this a module +x; f; C; E; N; +type U = T; type V = I;`, + + "/x.ts": +`export const x = 0; +export function f() { } +export class C { +} +export enum E { +} +export namespace N { + export const x = 0; +} +export type T = number; +export interface I { +}`, + }, +}); diff --git a/tests/cases/fourslash/moveToNewFile_defaultExport.ts b/tests/cases/fourslash/moveToNewFile_defaultExport.ts new file mode 100644 index 0000000000000..2a7e3e47a67c9 --- /dev/null +++ b/tests/cases/fourslash/moveToNewFile_defaultExport.ts @@ -0,0 +1,25 @@ +/// + +// @Filename: /a.ts +////[|export default function f() { }|] +////f(); + +// @Filename: /user.ts +////import f from "./a"; +////f(); + +verify.moveToNewFile({ + newFileContents: { + "/a.ts": +`import f from "./f"; + +f();`, + + "/f.ts": +`export default function f() { }`, + + "/user.ts": +`import f from "./f"; +f();`, + }, +}); diff --git a/tests/cases/fourslash/moveToNewFile_defaultImport.ts b/tests/cases/fourslash/moveToNewFile_defaultImport.ts new file mode 100644 index 0000000000000..031da380885eb --- /dev/null +++ b/tests/cases/fourslash/moveToNewFile_defaultImport.ts @@ -0,0 +1,17 @@ +/// + +// @Filename: /a.ts +////export default function f() { } +////[|const x = f();|] + +verify.moveToNewFile({ + newFileContents: { + "/a.ts": +`export default function f() { } +`, + + "/x.ts": +`import f from "./a"; +const x = f();`, + }, +}); diff --git a/tests/cases/fourslash/moveToNewFile_exportImport.ts b/tests/cases/fourslash/moveToNewFile_exportImport.ts new file mode 100644 index 0000000000000..a8ac3a66a0124 --- /dev/null +++ b/tests/cases/fourslash/moveToNewFile_exportImport.ts @@ -0,0 +1,22 @@ +/// + +// @Filename: /a.ts +////namespace N { export const x = 0; } +////[|import M = N; +////export import O = N;|] +////M; + +verify.moveToNewFile({ + newFileContents: { + "/a.ts": +`import { M } from "./M"; + +export namespace N { export const x = 0; } +M;`, + + "/M.ts": +`import { N } from "./a"; +export import M = N; +export import O = N;`, + }, +}); diff --git a/tests/cases/fourslash/moveToNewFile_global.ts b/tests/cases/fourslash/moveToNewFile_global.ts new file mode 100644 index 0000000000000..04443c50cb272 --- /dev/null +++ b/tests/cases/fourslash/moveToNewFile_global.ts @@ -0,0 +1,16 @@ +/// + +// @Filename: /a.ts +////const x = y; +////[|const y = x;|] + +verify.moveToNewFile({ + newFileContents: { + "/a.ts": +`const x = y; +`, + + "/y.ts": +`const y = x;`, + }, +}); diff --git a/tests/cases/fourslash/moveToNewFile_importEquals.ts b/tests/cases/fourslash/moveToNewFile_importEquals.ts new file mode 100644 index 0000000000000..a87c84c00046a --- /dev/null +++ b/tests/cases/fourslash/moveToNewFile_importEquals.ts @@ -0,0 +1,18 @@ +/// + +// @Filename: /a.ts +////import i = require("./i"); +////import j = require("./j"); +////[|const y = i;|] +////j; + +verify.moveToNewFile({ + newFileContents: { + "/a.ts": +`import j = require("./j"); +j;`, + "/y.ts": +`import i = require("./i"); +const y = i;`, + }, +}); diff --git a/tests/cases/fourslash/moveToNewFile_js.ts b/tests/cases/fourslash/moveToNewFile_js.ts new file mode 100644 index 0000000000000..e70bfa5f430c2 --- /dev/null +++ b/tests/cases/fourslash/moveToNewFile_js.ts @@ -0,0 +1,35 @@ +/// + +// @allowJs: true + +// @Filename: /a.js +////const { a, b } = require("./other"); +////const p = 0; +////[|const y = p + b; +////const z = 0; +////exports.z = 0;|] +////a; y; z; + +// @Filename: /user.ts +////const { x, y } = require("./a"); + +verify.moveToNewFile({ + newFileContents: { + "/a.js": +// TODO: GH#22330 +`const { y, z } = require("./y"); + +const { a, } = require("./other"); +const p = 0; +exports.p = p; +a; y; z;`, + + "/y.js": +`const { b } = require("./other"); +const { p } = require("./a"); +const y = p + b; +exports.y = y; +const z = 0; +exports.z = 0;`, + }, +}); diff --git a/tests/cases/fourslash/moveToNewFile_moveImport.ts b/tests/cases/fourslash/moveToNewFile_moveImport.ts new file mode 100644 index 0000000000000..a96a187f633f6 --- /dev/null +++ b/tests/cases/fourslash/moveToNewFile_moveImport.ts @@ -0,0 +1,18 @@ +/// + +// @Filename: /a.ts +////[|import { a, b } from "m"; +////a;|] +////b; + +//verify.noMoveToNewFile(); +verify.moveToNewFile({ + newFileContents: { + "/a.ts": +`b;`, + "/newFile.ts": +`import { a } from "m"; +import { a, b } from "m"; +a;`, + } +}); diff --git a/tests/cases/fourslash/moveToNewFile_multiple.ts b/tests/cases/fourslash/moveToNewFile_multiple.ts new file mode 100644 index 0000000000000..2a235ad9d8f70 --- /dev/null +++ b/tests/cases/fourslash/moveToNewFile_multiple.ts @@ -0,0 +1,28 @@ +/// + +// @Filename: /a.ts +////export {}; // make this a module +////const a = 0, b = 0; +////[|const x = 0; +////a; +////const y = 1; +////b;|] +////x; y; + +verify.moveToNewFile({ + newFileContents: { + "/a.ts": +`import { x, y } from "./x"; + +export {}; // make this a module +export const a = 0, b = 0; +x; y;`, + + "/x.ts": +`import { a, b } from "./a"; +export const x = 0; +a; +export const y = 1; +b;`, + }, +}); diff --git a/tests/cases/fourslash/moveToNewFile_newModuleNameUnique.ts b/tests/cases/fourslash/moveToNewFile_newModuleNameUnique.ts new file mode 100644 index 0000000000000..58cebf993daf2 --- /dev/null +++ b/tests/cases/fourslash/moveToNewFile_newModuleNameUnique.ts @@ -0,0 +1,20 @@ +/// + +// @Filename: /a.ts +////[|export const x = 0;|] + +// @Filename: /x.ts +//// + +// @Filename: /x.1.ts +//// + +verify.moveToNewFile({ + newFileContents: { + "/a.ts": +``, + + "/x.2.ts": +`export const x = 0;`, + }, +}); diff --git a/tests/cases/fourslash/moveToNewFile_onlyStatements.ts b/tests/cases/fourslash/moveToNewFile_onlyStatements.ts new file mode 100644 index 0000000000000..fe019d5e9afaf --- /dev/null +++ b/tests/cases/fourslash/moveToNewFile_onlyStatements.ts @@ -0,0 +1,16 @@ +/// + +// @Filename: /a.ts +////console.log("hello"); +////[|console.log("goodbye");|] + +verify.moveToNewFile({ + newFileContents: { + "/a.ts": +`console.log("hello"); +`, + + "/newFile.ts": +`console.log("goodbye");`, + }, +}); diff --git a/tests/cases/fourslash/moveToNewFile_rangeInvalid.ts b/tests/cases/fourslash/moveToNewFile_rangeInvalid.ts new file mode 100644 index 0000000000000..dc0ba661be48b --- /dev/null +++ b/tests/cases/fourslash/moveToNewFile_rangeInvalid.ts @@ -0,0 +1,10 @@ +/// + +// @Filename: /a.ts +////[|const x = 0; +////const|] y = 0; +////function f() { +//// [|function inner() {}|] +////} + +verify.noMoveToNewFile(); diff --git a/tests/cases/fourslash/moveToNewFile_rangeSemiValid.ts b/tests/cases/fourslash/moveToNewFile_rangeSemiValid.ts new file mode 100644 index 0000000000000..0f5636f33cff8 --- /dev/null +++ b/tests/cases/fourslash/moveToNewFile_rangeSemiValid.ts @@ -0,0 +1,19 @@ +/// + +// @Filename: /a.ts +////[|const x = 0; +//// +/////** Comm|]ent */ +////const y = 0; + +verify.moveToNewFile({ + newFileContents: { + "/a.ts": +` +/** Comment */ +const y = 0;`, + + "/x.ts": +`const x = 0;`, + }, +}); diff --git a/tests/cases/fourslash/moveToNewFile_tsconfig.ts b/tests/cases/fourslash/moveToNewFile_tsconfig.ts new file mode 100644 index 0000000000000..264f2a85ef22f --- /dev/null +++ b/tests/cases/fourslash/moveToNewFile_tsconfig.ts @@ -0,0 +1,28 @@ +/// + +// @Filename: /src/a.ts +////0; +////[|1;|] + +// @Filename: /src/tsconfig.json +////{ +//// "files": ["./a.ts"] +////} + +verify.noErrors(); + +verify.moveToNewFile({ + newFileContents: { + "/src/a.ts": +`0; +`, + + "/src/newFile.ts": +`1;`, + + "/src/tsconfig.json": +`{ + "files": ["./a.ts", "./newFile.ts"] +}`, + }, +}); diff --git a/tests/cases/fourslash/moveToNewFile_updateUses.ts b/tests/cases/fourslash/moveToNewFile_updateUses.ts new file mode 100644 index 0000000000000..b067889625eda --- /dev/null +++ b/tests/cases/fourslash/moveToNewFile_updateUses.ts @@ -0,0 +1,27 @@ +/// + +// @Filename: /a.ts +////export const x = 0; +////[|export const y = 0;|] + +// @Filename: /user.ts +////import { x, y } from "./a"; +//// + +// TODO: GH#23728 Shouldn't need `////` above + +verify.moveToNewFile({ + newFileContents: { + "/a.ts": +`export const x = 0; +`, + + "/y.ts": +`export const y = 0;`, + + "/user.ts": +`import { x } from "./a"; +import { y } from "./y"; +`, + }, +}); diff --git a/tests/cases/fourslash/moveToNewFile_updateUses_js.ts b/tests/cases/fourslash/moveToNewFile_updateUses_js.ts new file mode 100644 index 0000000000000..09fbf32a31335 --- /dev/null +++ b/tests/cases/fourslash/moveToNewFile_updateUses_js.ts @@ -0,0 +1,30 @@ +/// + +// @allowJs: true + +// @Filename: /a.js +////exports.x = 0; +////[|exports.y = 0;|] + +// @Filename: /user.js +////const { x, y } = require("./a"); +//// + +// TODO: GH#23728 Shouldn't need `////` above + +verify.moveToNewFile({ + newFileContents: { + "/a.js": +`exports.x = 0; +`, + + "/y.js": +`exports.y = 0;`, + + "/user.js": +// TODO: GH#22330 +`const { x, } = require("./a"); +const { y } = require("./y"); +`, + }, +});