FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

Add 'move to new file' refactor · Pull Request #23726 · microsoft/TypeScript · GitHub

Add 'move to new file' refactor - #23726

Merged
15 commits merged into
masterfrom
moveToNewFile
May 10, 2018
Merged

Add 'move to new file' refactor#23726
15 commits merged into
masterfrom
moveToNewFile

Conversation

Deleted user (ghost) commented Apr 26, 2018
edited by ghost
Loading

Copy link
Copy Markdown

The user may highlight declarations on the top-level of a file and move them to a new file. Imports will be updated, and declarations will be exported as necessary (if they were private in the old file but used in the new file, or vice-versa).

This doesn't work for nested declarations because those may need to close over things -- extractSymbol handles that and adds parameters as necessary. So it would take two refactorings to take something from an inner scope to a different file.

ghost force-pushed the moveToNewFile branch from 8f62d5c to 76871d2 Compare April 26, 2018 23:26
Comment thread src/compiler/diagnosticMessages.json Outdated
"category": "Message",
"code": 95046
},
"Move to new file": {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Move to a new file ?

});
}

export function newFileChanges(oldFile: SourceFile, fileName: string, statements: ReadonlyArray<Statement>, newLineCharacter: string): FileTextChanges {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Can you check with Matt Bierner (@mjbvz) and Andrew Casey (@amcasey) that sending edits for non-existing files are ok on the editor sides.

const range = createTextRangeFromSpan(getRefactorContextSpan(context));
const { statements } = file;

const startNodeIndex = findIndex(statements, s => s.end > range.pos);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

so that means that if i select a nested function to move, we will move the containing function instead.. would not it be better to not make the refactoring available at this case?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

See moveToNewFile_rangeInvalid.ts.

Comment thread src/services/refactors/moveToNewFile.ts Outdated
): ReadonlyArray<Statement> {
const checker = program.getTypeChecker();

if (!oldFile.externalModuleIndicator) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

what if this is a .js file with require calls?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

also what should we do with require statements in a .js file. seems like we have enough information in the current file to know if we should create import or const x = require(...)..

Comment thread src/services/refactors/moveToNewFile.ts Outdated
});
}

function deleteUnusedImports(sourceFile: SourceFile, importDecl: ImportDeclaration, changes: textChanges.ChangeTracker, isUnused: (name: Identifier) => boolean): void {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

this looks a lot like the logic we use in fixUnusedIdentifer for module imports.. can we consolidate?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

I don't think that's easy since fixUnusedIdentifier deletes a single identifier starting from the identifier, but here we have a set of symbols and want to recurse down starting from the sourcefile and delete everything in the set.

Comment thread src/services/refactors/moveToNewFile.ts Outdated

function deleteUnusedOldImports(oldFile: SourceFile, toMove: ReadonlyArray<Statement>, changes: textChanges.ChangeTracker, toDelete: ReadonlySymbolSet, checker: TypeChecker) {
for (const statement of oldFile.statements) {
if (!contains(toMove, statement) && isImportDeclaration(statement)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

what about ImportEqualsDeclaration?

Comment thread src/services/refactors/moveToNewFile.ts Outdated
while (true) {
const name = combinePaths(inDirectory, moduleName + extension);
if (!host.fileExists(name)) return moduleName;
moduleName += "0";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

nit, i would make that 1-based (more natural) and i would use a separator of sorts. VSCode adds a .1, Windows will add (2) to duplicate files.
we could go with the .1 or we can use _1.

Comment thread src/services/refactors/moveToNewFile.ts Outdated

function getNewModuleName(movedSymbols: ReadonlySymbolSet): string {
let name: string | undefined;
movedSymbols.forEach(s => { if (name === undefined) name = symbolNameNoDefault(s); });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality
return forEach(movedSymbols, symbolNameNoDefault) || "newFile";

Comment thread src/services/refactors/moveToNewFile.ts Outdated
});

const oldFileImport = makeImportIfNecessary(oldFileDefault, oldFileNamedImports, `./${removeFileExtension(getBaseFileName(oldFile.fileName))}`);
return [...copiedOldImports, ...(oldFileImport ? [oldFileImport] : emptyArray)];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

nit, why not just push on copiedOldImports instead of cloning it.

if (isInImport(decl)) {
oldImportsNeededByNewFile.add(symbol);
}
else if (isTopLevelDeclaration(decl) && !movedSymbols.has(symbol)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

what if it is not in movedSymbols just case we have not visited it yet, e.g. a forward reference to a type or an interface.. or even a class, that exists later on in the selected range?

function addEs6Export(d: TopLevelDeclarationStatement): TopLevelDeclarationStatement {
const modifiers = concatenate([createModifier(SyntaxKind.ExportKeyword)], d.modifiers);
switch (d.kind) {
case SyntaxKind.FunctionDeclaration:

Mohamed Hegazy (mhegazy) May 1, 2018
edited
Loading

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

also export import A = N

Mohamed Hegazy (mhegazy) left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Please check with Andrew Casey (@amcasey) and Matt Bierner (@mjbvz) regarding new file changes.

ghost force-pushed the moveToNewFile branch from a09c2f7 to 7f40d09 Compare May 1, 2018 18:54

Copy link
Copy Markdown
Contributor

Fixes #13859

Copy link
Copy Markdown
Author

New commit fixes #23793

Comment thread src/compiler/factory.ts Outdated
node.expression = parenthesizeExpressionForExpressionStatement(expression);
return node;
return createExpressionStatement(parenthesizeExpressionForExpressionStatement(expression));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Nit: extra newline.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

We could consider linting for no-padding.

Copy link
Copy Markdown
Contributor

Andy (Andrewkraft) (@Andy-MS) we need to make this opt-in to avoid old versions of VS/VSCode form getting edits including new files.
so we need a new setting, allowNewFileEdits or something along these lines.

Copy link
Copy Markdown
Contributor

we also need to add the file to "files" in tsconfig.json if the original file was in the list.

Copy link
Copy Markdown
Author

Latest commit adds an allowTextChangesInNewFiles preference since older editors might have problems with the new behavior. CC Matt Bierner (@mjbvz)

Comment thread src/compiler/core.ts Outdated
/** Works like Array.prototype.findIndex, returning `-1` if no element satisfying the predicate is found. */
export function findIndex<T>(array: ReadonlyArray<T>, predicate: (element: T, index: number) => boolean): number {
for (let i = 0; i < array.length; i++) {
export function findIndex<T>(array: ReadonlyArray<T>, predicate: (element: T, index: number) => boolean, startIndex = 0): number {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

can you instead make it startIndex?: number and use let i = startIndex || 0 ?

Comment thread src/services/refactors/moveToNewFile.ts Outdated
// If previous file was global, this is easy.
changes.createNewFile(oldFile, combinePaths(currentDirectory, newFileNameWithExtension), getNewStatements(oldFile, usage, changes, toMove, program, newModuleName));

addNewFileToTsconfig(program, changes, normalizePath(combinePaths(oldFile.fileName, "..", newFileNameWithExtension)));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

should not that be the filename relative to the tsconfig.json path?

Andrew Casey (amcasey) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Initial impressions

Comment thread src/services/refactors/moveToNewFile.ts Outdated
const refactorName = "Move to a new file";
registerRefactor(refactorName, {
getAvailableActions(context): ApplicableRefactorInfo[] {
if (getStatementsToMove(context) === undefined || !context.preferences.allowTextChangesInNewFiles) return undefined;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

The second check looks cheaper. Would it make sense to do that first?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

👍

}
});

function getStatementsToMove(context: RefactorContext): ReadonlyArray<Statement> | undefined {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Could/should this logic be shared with extract function/constant?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

The corresponding function there would be getRangeToExtract. That seems to have a lot of extract-symbol-specific logic in it, though. And here we should only be moving top-level statements.

];
}

function deleteUnusedOldImports(oldFile: SourceFile, toMove: ReadonlyArray<Statement>, changes: textChanges.ChangeTracker, toDelete: ReadonlySymbolSet, checker: TypeChecker) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

I think the goal of this function is to remove the imports that were only needed for the code being moved. It seems though, like it might also remove imports that were unused to begin with. Personally, I think it seems strange to do a partial Organize Imports as part of this operation. If we are going to do so, then my preference would be to have the editor trigger (unless the server has some extra knowledge?) so that it can appear separately on the undo stack.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

unusedImportsFromOldFile will be a subset of oldImportsNeededByNewFile, so we won't remove purely-unused imports.

Comment thread tests/cases/fourslash/moveToNewFile.ts Outdated
/// <reference path='fourslash.ts' />

// @Filename: /a.ts
////import { a, b } from "./other";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

[I'm sure this is clear from the product code, but I haven't read it in detail.] What happens if you extract an import statement. Is that disallowed?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

What about a return statement? Can a return statement be moved to another file?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

What happens if you extract an import statement

I think we should allow this, though there's a bug if the moved import is still needed in the old file. #23968

Can a return statement be moved to another file?

Only top-level statements can be moved, and 'return' at top-level is an errorany way.

ghost force-pushed the moveToNewFile branch from 7b9cbfe to 662e93c Compare May 8, 2018 19:45

Copy link
Copy Markdown
Author

Andrew Casey (@amcasey) Good to go?

This pull request was closed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants


Back | FazBrowse Home | New Git URL