diff --git a/src/services/refactors/moveToFile.ts b/src/services/refactors/moveToFile.ts index 80064ab1489..64fe7b0a4ae 100644 --- a/src/services/refactors/moveToFile.ts +++ b/src/services/refactors/moveToFile.ts @@ -893,12 +893,10 @@ export interface TopLevelVariableDeclaration extends VariableDeclaration { export type TopLevelDeclaration = NonVariableTopLevelDeclaration | TopLevelVariableDeclaration | BindingElement; /** @internal */ -export function createNewFileName(oldFile: SourceFile, program: Program, context: RefactorContext, host: LanguageServiceHost): string { +export function createNewFileName(oldFile: SourceFile, program: Program, host: LanguageServiceHost, toMove: ToMove | undefined): string { const checker = program.getTypeChecker(); - const toMove = getStatementsToMove(context); - let usage; if (toMove) { - usage = getUsageInfo(oldFile, toMove.all, checker); + const usage = getUsageInfo(oldFile, toMove.all, checker); const currentDirectory = getDirectoryPath(oldFile.fileName); const extension = extensionFromPath(oldFile.fileName); const newFileName = combinePaths( @@ -974,6 +972,11 @@ export function getStatementsToMove(context: RefactorContext): ToMove | undefine return all.length === 0 ? undefined : { all, ranges }; } +/** @internal */ +export function containsJsx(statements: readonly Statement[] | undefined) { + return find(statements, statement => !!(statement.transformFlags & TransformFlags.ContainsJsx)); +} + function isAllowedStatementToMove(statement: Statement): boolean { // Filters imports and prologue directives out of the range of statements to move. // Imports will be copied to the new file anyway, and may still be needed in the old file. @@ -1000,8 +1003,7 @@ export function getUsageInfo(oldFile: SourceFile, toMove: readonly Statement[], const oldImportsNeededByTargetFile = new Map(); const targetFileImportsFromOldFile = new Set(); - const containsJsx = find(toMove, statement => !!(statement.transformFlags & TransformFlags.ContainsJsx)); - const jsxNamespaceSymbol = getJsxNamespaceSymbol(containsJsx); + const jsxNamespaceSymbol = getJsxNamespaceSymbol(containsJsx(toMove)); if (jsxNamespaceSymbol) { // Might not exist (e.g. in non-compiling code) oldImportsNeededByTargetFile.set(jsxNamespaceSymbol, false); diff --git a/src/services/refactors/moveToNewFile.ts b/src/services/refactors/moveToNewFile.ts index 275ed16bd5a..d2cb3608449 100644 --- a/src/services/refactors/moveToNewFile.ts +++ b/src/services/refactors/moveToNewFile.ts @@ -18,7 +18,6 @@ import { nodeSeenTracker, Program, QuotePreference, - RefactorContext, RefactorEditInfo, SourceFile, Symbol, @@ -75,16 +74,15 @@ registerRefactor(refactorName, { getEditsForAction: function getRefactorEditsToMoveToNewFile(context, actionName): RefactorEditInfo { Debug.assert(actionName === refactorName, "Wrong refactor invoked"); const statements = Debug.checkDefined(getStatementsToMove(context)); - const edits = textChanges.ChangeTracker.with(context, t => doChange(context.file, context.program, statements, t, context.host, context.preferences, context)); + const edits = textChanges.ChangeTracker.with(context, t => doChange(context.file, context.program, statements, t, context.host, context.preferences)); return { edits, renameFilename: undefined, renameLocation: undefined }; }, }); -function doChange(oldFile: SourceFile, program: Program, toMove: ToMove, changes: textChanges.ChangeTracker, host: LanguageServiceHost, preferences: UserPreferences, context: RefactorContext): void { +function doChange(oldFile: SourceFile, program: Program, toMove: ToMove, changes: textChanges.ChangeTracker, host: LanguageServiceHost, preferences: UserPreferences): void { const checker = program.getTypeChecker(); const usage = getUsageInfo(oldFile, toMove.all, checker); - - const newFilename = createNewFileName(oldFile, program, context, host); + const newFilename = createNewFileName(oldFile, program, host, toMove); // If previous file was global, this is easy. changes.createNewFile(oldFile, newFilename, getNewStatementsAndRemoveFromOldFile(oldFile, usage, changes, toMove, program, host, newFilename, preferences)); diff --git a/src/services/services.ts b/src/services/services.ts index 924828da0aa..32a54a89724 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -323,7 +323,9 @@ import { import * as NavigateTo from "./_namespaces/ts.NavigateTo"; import * as NavigationBar from "./_namespaces/ts.NavigationBar"; import { + containsJsx, createNewFileName, + getStatementsToMove, } from "./_namespaces/ts.refactor"; import * as classifier from "./classifier"; import * as classifier2020 from "./classifier2020"; @@ -3030,13 +3032,20 @@ export function createLanguageService( const sourceFile = getValidSourceFile(fileName); const allFiles = Debug.checkDefined(program.getSourceFiles()); const extension = extensionFromPath(fileName); - const files = mapDefined(allFiles, file => - !program?.isSourceFileFromExternalLibrary(sourceFile) && - !(sourceFile === getValidSourceFile(file.fileName) || extension === Extension.Ts && extensionFromPath(file.fileName) === Extension.Dts || extension === Extension.Dts && startsWith(getBaseFileName(file.fileName), "lib.") && extensionFromPath(file.fileName) === Extension.Dts) - && extension === extensionFromPath(file.fileName) ? file.fileName : undefined); + const toMove = getStatementsToMove(getRefactorContext(sourceFile, positionOrRange, preferences, emptyOptions)); + const toMoveContainsJsx = containsJsx(toMove?.all); - const newFileName = createNewFileName(sourceFile, program, getRefactorContext(sourceFile, positionOrRange, preferences, emptyOptions), host); - return { newFileName, files }; + const files = mapDefined(allFiles, file => { + const fileNameExtension = extensionFromPath(file.fileName); + const isValidSourceFile = !program?.isSourceFileFromExternalLibrary(sourceFile) && !( + sourceFile === getValidSourceFile(file.fileName) || + extension === Extension.Ts && fileNameExtension === Extension.Dts || + extension === Extension.Dts && startsWith(getBaseFileName(file.fileName), "lib.") && fileNameExtension === Extension.Dts + ); + return isValidSourceFile && (extension === fileNameExtension || (extension === Extension.Tsx && fileNameExtension === Extension.Ts || extension === Extension.Jsx && fileNameExtension === Extension.Js) && !toMoveContainsJsx) ? file.fileName : undefined; + }); + + return { newFileName: createNewFileName(sourceFile, program, host, toMove), files }; } function getEditsForRefactor( diff --git a/src/testRunner/unittests/tsserver/getMoveToRefactoringFileSuggestions.ts b/src/testRunner/unittests/tsserver/getMoveToRefactoringFileSuggestions.ts index 967dd319181..ace602019cc 100644 --- a/src/testRunner/unittests/tsserver/getMoveToRefactoringFileSuggestions.ts +++ b/src/testRunner/unittests/tsserver/getMoveToRefactoringFileSuggestions.ts @@ -116,4 +116,61 @@ import { value1 } from "../node_modules/.cache/someFile.d.ts";`, }); baselineTsserverLogs("getMoveToRefactoringFileSuggestions", "skips lib.d.ts files", session); }); + + it("should show ts files when moving non-tsx content from tsx file", () => { + const file1: File = { + path: "/bar.tsx", + content: `export function bar() { }`, + }; + const file2: File = { + path: "/foo.ts", + content: "export function foo() { }", + }; + const tsconfig: File = { + path: "/tsconfig.json", + content: jsonToReadableText({ + jsx: "preserve", + files: ["./foo.ts", "./bar.tsx"], + }), + }; + + const host = createServerHost([file1, file2, tsconfig]); + const session = new TestSession(host); + openFilesForSession([file1], session); + + session.executeCommandSeq({ + command: ts.server.protocol.CommandTypes.GetMoveToRefactoringFileSuggestions, + arguments: { file: file1.path, line: 1, offset: 7 }, + }); + baselineTsserverLogs("getMoveToRefactoringFileSuggestions", "should show ts files when moving non-tsx content from tsx file", session); + }); + + it("should show js files when moving non-jsx content from jsx file", () => { + const file1: File = { + path: "/bar.jsx", + content: `export function bar() { }`, + }; + const file2: File = { + path: "/foo.js", + content: "export function foo() { }", + }; + const tsconfig: File = { + path: "/tsconfig.json", + content: jsonToReadableText({ + allowJS: true, + jsx: "preserve", + files: ["./foo.js", "./bar.jsx"], + }), + }; + + const host = createServerHost([file1, file2, tsconfig]); + const session = new TestSession(host); + openFilesForSession([file1], session); + + session.executeCommandSeq({ + command: ts.server.protocol.CommandTypes.GetMoveToRefactoringFileSuggestions, + arguments: { file: file1.path, line: 1, offset: 7 }, + }); + baselineTsserverLogs("getMoveToRefactoringFileSuggestions", "should show js files when moving non-jsx content from jsx file", session); + }); }); diff --git a/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/should-show-js-files-when-moving-non-jsx-content-from-jsx-file.js b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/should-show-js-files-when-moving-non-jsx-content-from-jsx-file.js new file mode 100644 index 00000000000..71ebf643c3a --- /dev/null +++ b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/should-show-js-files-when-moving-non-jsx-content-from-jsx-file.js @@ -0,0 +1,256 @@ +currentDirectory:: / useCaseSensitiveFileNames: false +Info seq [hh:mm:ss:mss] Provided types map file "/typesMap.json" doesn't exist +Before request +//// [/bar.jsx] +export function bar() { } + +//// [/foo.js] +export function foo() { } + +//// [/tsconfig.json] +{ + "allowJS": true, + "jsx": "preserve", + "files": [ + "./foo.js", + "./bar.jsx" + ] +} + + +Info seq [hh:mm:ss:mss] request: + { + "command": "open", + "arguments": { + "file": "/bar.jsx" + }, + "seq": 1, + "type": "request" + } +Info seq [hh:mm:ss:mss] Search path: / +Info seq [hh:mm:ss:mss] For info: /bar.jsx :: Config file name: /tsconfig.json +Info seq [hh:mm:ss:mss] Creating configuration project /tsconfig.json +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /tsconfig.json 2000 undefined Project: /tsconfig.json WatchType: Config file +Info seq [hh:mm:ss:mss] event: + { + "seq": 0, + "type": "event", + "event": "projectLoadingStart", + "body": { + "projectName": "/tsconfig.json", + "reason": "Creating possible configured project for /bar.jsx to open" + } + } +Info seq [hh:mm:ss:mss] Config: /tsconfig.json : { + "rootNames": [ + "/foo.js", + "/bar.jsx" + ], + "options": { + "configFilePath": "/tsconfig.json" + } +} +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /foo.js 500 undefined WatchType: Closed Script info +Info seq [hh:mm:ss:mss] Starting updateGraphWorker: Project: /tsconfig.json +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 500 undefined Project: /tsconfig.json WatchType: Missing file +Info seq [hh:mm:ss:mss] Finishing updateGraphWorker: Project: /tsconfig.json projectStateVersion: 1 projectProgramVersion: 0 structureChanged: true structureIsReused:: Not Elapsed:: *ms +Info seq [hh:mm:ss:mss] Project '/tsconfig.json' (Configured) +Info seq [hh:mm:ss:mss] Files (2) + /foo.js Text-1 "export function foo() { }" + /bar.jsx SVC-1-0 "export function bar() { }" + + + foo.js + Part of 'files' list in tsconfig.json + bar.jsx + Part of 'files' list in tsconfig.json + +Info seq [hh:mm:ss:mss] ----------------------------------------------- +Info seq [hh:mm:ss:mss] event: + { + "seq": 0, + "type": "event", + "event": "projectLoadingFinish", + "body": { + "projectName": "/tsconfig.json" + } + } +Info seq [hh:mm:ss:mss] event: + { + "seq": 0, + "type": "event", + "event": "telemetry", + "body": { + "telemetryEventName": "projectInfo", + "payload": { + "projectId": "aace87d7c1572ff43c6978074161b586788b4518c7a9d06c79c03e613b6ce5a3", + "fileStats": { + "js": 1, + "jsSize": 25, + "jsx": 1, + "jsxSize": 25, + "ts": 0, + "tsSize": 0, + "tsx": 0, + "tsxSize": 0, + "dts": 0, + "dtsSize": 0, + "deferred": 0, + "deferredSize": 0 + }, + "compilerOptions": {}, + "typeAcquisition": { + "enable": false, + "include": false, + "exclude": false + }, + "extends": false, + "files": true, + "include": false, + "exclude": false, + "compileOnSave": false, + "configFileName": "tsconfig.json", + "projectType": "configured", + "languageServiceEnabled": true, + "version": "FakeVersion" + } + } + } +Info seq [hh:mm:ss:mss] event: + { + "seq": 0, + "type": "event", + "event": "configFileDiag", + "body": { + "triggerFile": "/bar.jsx", + "configFile": "/tsconfig.json", + "diagnostics": [ + { + "text": "Cannot write file '/foo.js' because it would overwrite input file.", + "code": 5055, + "category": "error" + }, + { + "text": "File '/a/lib/lib.d.ts' not found.\n The file is in the program because:\n Default library for target 'es5'", + "code": 6053, + "category": "error" + }, + { + "text": "Cannot find global type 'Array'.", + "code": 2318, + "category": "error" + }, + { + "text": "Cannot find global type 'Boolean'.", + "code": 2318, + "category": "error" + }, + { + "text": "Cannot find global type 'Function'.", + "code": 2318, + "category": "error" + }, + { + "text": "Cannot find global type 'IArguments'.", + "code": 2318, + "category": "error" + }, + { + "text": "Cannot find global type 'Number'.", + "code": 2318, + "category": "error" + }, + { + "text": "Cannot find global type 'Object'.", + "code": 2318, + "category": "error" + }, + { + "text": "Cannot find global type 'RegExp'.", + "code": 2318, + "category": "error" + }, + { + "text": "Cannot find global type 'String'.", + "code": 2318, + "category": "error" + }, + { + "start": { + "line": 3, + "offset": 3 + }, + "end": { + "line": 3, + "offset": 8 + }, + "text": "'jsx' should be set inside the 'compilerOptions' object of the config json file", + "code": 6258, + "category": "error", + "fileName": "/tsconfig.json" + } + ] + } + } +Info seq [hh:mm:ss:mss] Project '/tsconfig.json' (Configured) +Info seq [hh:mm:ss:mss] Files (2) + +Info seq [hh:mm:ss:mss] ----------------------------------------------- +Info seq [hh:mm:ss:mss] Open files: +Info seq [hh:mm:ss:mss] FileName: /bar.jsx ProjectRootPath: undefined +Info seq [hh:mm:ss:mss] Projects: /tsconfig.json +Info seq [hh:mm:ss:mss] response: + { + "responseRequired": false + } +After request + +PolledWatches:: +/a/lib/lib.d.ts: *new* + {"pollingInterval":500} + +FsWatches:: +/foo.js: *new* + {} +/tsconfig.json: *new* + {} + +Projects:: +/tsconfig.json (Configured) *new* + projectStateVersion: 1 + projectProgramVersion: 1 + +ScriptInfos:: +/bar.jsx (Open) *new* + version: SVC-1-0 + containingProjects: 1 + /tsconfig.json *default* +/foo.js *new* + version: Text-1 + containingProjects: 1 + /tsconfig.json + +Before request + +Info seq [hh:mm:ss:mss] request: + { + "command": "getMoveToRefactoringFileSuggestions", + "arguments": { + "file": "/bar.jsx", + "line": 1, + "offset": 7 + }, + "seq": 2, + "type": "request" + } +Info seq [hh:mm:ss:mss] response: + { + "response": { + "newFileName": "/bar.1.jsx", + "files": [ + "/foo.js" + ] + }, + "responseRequired": true + } +After request diff --git a/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/should-show-ts-files-when-moving-non-tsx-content-from-tsx-file.js b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/should-show-ts-files-when-moving-non-tsx-content-from-tsx-file.js new file mode 100644 index 00000000000..a9b47279462 --- /dev/null +++ b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/should-show-ts-files-when-moving-non-tsx-content-from-tsx-file.js @@ -0,0 +1,250 @@ +currentDirectory:: / useCaseSensitiveFileNames: false +Info seq [hh:mm:ss:mss] Provided types map file "/typesMap.json" doesn't exist +Before request +//// [/bar.tsx] +export function bar() { } + +//// [/foo.ts] +export function foo() { } + +//// [/tsconfig.json] +{ + "jsx": "preserve", + "files": [ + "./foo.ts", + "./bar.tsx" + ] +} + + +Info seq [hh:mm:ss:mss] request: + { + "command": "open", + "arguments": { + "file": "/bar.tsx" + }, + "seq": 1, + "type": "request" + } +Info seq [hh:mm:ss:mss] Search path: / +Info seq [hh:mm:ss:mss] For info: /bar.tsx :: Config file name: /tsconfig.json +Info seq [hh:mm:ss:mss] Creating configuration project /tsconfig.json +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /tsconfig.json 2000 undefined Project: /tsconfig.json WatchType: Config file +Info seq [hh:mm:ss:mss] event: + { + "seq": 0, + "type": "event", + "event": "projectLoadingStart", + "body": { + "projectName": "/tsconfig.json", + "reason": "Creating possible configured project for /bar.tsx to open" + } + } +Info seq [hh:mm:ss:mss] Config: /tsconfig.json : { + "rootNames": [ + "/foo.ts", + "/bar.tsx" + ], + "options": { + "configFilePath": "/tsconfig.json" + } +} +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /foo.ts 500 undefined WatchType: Closed Script info +Info seq [hh:mm:ss:mss] Starting updateGraphWorker: Project: /tsconfig.json +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 500 undefined Project: /tsconfig.json WatchType: Missing file +Info seq [hh:mm:ss:mss] Finishing updateGraphWorker: Project: /tsconfig.json projectStateVersion: 1 projectProgramVersion: 0 structureChanged: true structureIsReused:: Not Elapsed:: *ms +Info seq [hh:mm:ss:mss] Project '/tsconfig.json' (Configured) +Info seq [hh:mm:ss:mss] Files (2) + /foo.ts Text-1 "export function foo() { }" + /bar.tsx SVC-1-0 "export function bar() { }" + + + foo.ts + Part of 'files' list in tsconfig.json + bar.tsx + Part of 'files' list in tsconfig.json + +Info seq [hh:mm:ss:mss] ----------------------------------------------- +Info seq [hh:mm:ss:mss] event: + { + "seq": 0, + "type": "event", + "event": "projectLoadingFinish", + "body": { + "projectName": "/tsconfig.json" + } + } +Info seq [hh:mm:ss:mss] event: + { + "seq": 0, + "type": "event", + "event": "telemetry", + "body": { + "telemetryEventName": "projectInfo", + "payload": { + "projectId": "aace87d7c1572ff43c6978074161b586788b4518c7a9d06c79c03e613b6ce5a3", + "fileStats": { + "js": 0, + "jsSize": 0, + "jsx": 0, + "jsxSize": 0, + "ts": 1, + "tsSize": 25, + "tsx": 1, + "tsxSize": 25, + "dts": 0, + "dtsSize": 0, + "deferred": 0, + "deferredSize": 0 + }, + "compilerOptions": {}, + "typeAcquisition": { + "enable": false, + "include": false, + "exclude": false + }, + "extends": false, + "files": true, + "include": false, + "exclude": false, + "compileOnSave": false, + "configFileName": "tsconfig.json", + "projectType": "configured", + "languageServiceEnabled": true, + "version": "FakeVersion" + } + } + } +Info seq [hh:mm:ss:mss] event: + { + "seq": 0, + "type": "event", + "event": "configFileDiag", + "body": { + "triggerFile": "/bar.tsx", + "configFile": "/tsconfig.json", + "diagnostics": [ + { + "text": "File '/a/lib/lib.d.ts' not found.\n The file is in the program because:\n Default library for target 'es5'", + "code": 6053, + "category": "error" + }, + { + "text": "Cannot find global type 'Array'.", + "code": 2318, + "category": "error" + }, + { + "text": "Cannot find global type 'Boolean'.", + "code": 2318, + "category": "error" + }, + { + "text": "Cannot find global type 'Function'.", + "code": 2318, + "category": "error" + }, + { + "text": "Cannot find global type 'IArguments'.", + "code": 2318, + "category": "error" + }, + { + "text": "Cannot find global type 'Number'.", + "code": 2318, + "category": "error" + }, + { + "text": "Cannot find global type 'Object'.", + "code": 2318, + "category": "error" + }, + { + "text": "Cannot find global type 'RegExp'.", + "code": 2318, + "category": "error" + }, + { + "text": "Cannot find global type 'String'.", + "code": 2318, + "category": "error" + }, + { + "start": { + "line": 2, + "offset": 3 + }, + "end": { + "line": 2, + "offset": 8 + }, + "text": "'jsx' should be set inside the 'compilerOptions' object of the config json file", + "code": 6258, + "category": "error", + "fileName": "/tsconfig.json" + } + ] + } + } +Info seq [hh:mm:ss:mss] Project '/tsconfig.json' (Configured) +Info seq [hh:mm:ss:mss] Files (2) + +Info seq [hh:mm:ss:mss] ----------------------------------------------- +Info seq [hh:mm:ss:mss] Open files: +Info seq [hh:mm:ss:mss] FileName: /bar.tsx ProjectRootPath: undefined +Info seq [hh:mm:ss:mss] Projects: /tsconfig.json +Info seq [hh:mm:ss:mss] response: + { + "responseRequired": false + } +After request + +PolledWatches:: +/a/lib/lib.d.ts: *new* + {"pollingInterval":500} + +FsWatches:: +/foo.ts: *new* + {} +/tsconfig.json: *new* + {} + +Projects:: +/tsconfig.json (Configured) *new* + projectStateVersion: 1 + projectProgramVersion: 1 + +ScriptInfos:: +/bar.tsx (Open) *new* + version: SVC-1-0 + containingProjects: 1 + /tsconfig.json *default* +/foo.ts *new* + version: Text-1 + containingProjects: 1 + /tsconfig.json + +Before request + +Info seq [hh:mm:ss:mss] request: + { + "command": "getMoveToRefactoringFileSuggestions", + "arguments": { + "file": "/bar.tsx", + "line": 1, + "offset": 7 + }, + "seq": 2, + "type": "request" + } +Info seq [hh:mm:ss:mss] response: + { + "response": { + "newFileName": "/bar.1.tsx", + "files": [ + "/foo.ts" + ] + }, + "responseRequired": true + } +After request