Merge pull request #21909 from amcasey/OrganizeImports

Introduce an organizeImports command
This commit is contained in:
Andrew Casey
2018-02-16 12:49:02 -08:00
committed by GitHub
20 changed files with 843 additions and 0 deletions

View File

@@ -141,6 +141,7 @@ var harnessSources = harnessCoreSources.concat([
"typingsInstaller.ts",
"projectErrors.ts",
"matchFiles.ts",
"organizeImports.ts",
"initializeTSConfig.ts",
"extractConstants.ts",
"extractFunctions.ts",

View File

@@ -1905,6 +1905,11 @@ namespace ts {
Comparison.EqualTo;
}
/** True is greater than false. */
export function compareBooleans(a: boolean, b: boolean): Comparison {
return compareValues(a ? 1 : 0, b ? 1 : 0);
}
function compareMessageText(text1: string | DiagnosticMessageChain, text2: string | DiagnosticMessageChain): Comparison {
while (text1 && text2) {
// We still have both chains.

View File

@@ -522,6 +522,9 @@ namespace Harness.LanguageService {
getApplicableRefactors(): ts.ApplicableRefactorInfo[] {
throw new Error("Not supported on the shim.");
}
organizeImports(_scope: ts.OrganizeImportsScope, _formatOptions: ts.FormatCodeSettings): ReadonlyArray<ts.FileTextChanges> {
throw new Error("Not supported on the shim.");
}
getEmitOutput(fileName: string): ts.EmitOutput {
return unwrapJSONCallResult(this.shim.getEmitOutput(fileName));
}

View File

@@ -117,6 +117,7 @@
"./unittests/tsserverProjectSystem.ts",
"./unittests/tscWatchMode.ts",
"./unittests/matchFiles.ts",
"./unittests/organizeImports.ts",
"./unittests/initializeTSConfig.ts",
"./unittests/compileOnSave.ts",
"./unittests/typingsInstaller.ts",

View File

@@ -0,0 +1,426 @@
/// <reference path="..\..\..\src\harness\harness.ts" />
/// <reference path="..\..\..\src\harness\virtualFileSystem.ts" />
namespace ts {
describe("Organize imports", () => {
describe("Sort imports", () => {
it("No imports", () => {
assert.isEmpty(OrganizeImports.sortImports([]));
});
it("One import", () => {
const unsortedImports = parseImports(`import "lib";`);
const actualSortedImports = OrganizeImports.sortImports(unsortedImports);
const expectedSortedImports = unsortedImports;
assertListEqual(expectedSortedImports, actualSortedImports);
});
it("Stable - import kind", () => {
assertUnaffectedBySort(
`import "lib";`,
`import * as x from "lib";`,
`import x from "lib";`,
`import {x} from "lib";`);
});
it("Stable - default property alias", () => {
assertUnaffectedBySort(
`import x from "lib";`,
`import y from "lib";`);
});
it("Stable - module alias", () => {
assertUnaffectedBySort(
`import * as x from "lib";`,
`import * as y from "lib";`);
});
it("Stable - symbol", () => {
assertUnaffectedBySort(
`import {x} from "lib";`,
`import {y} from "lib";`);
});
it("Sort - non-relative vs non-relative", () => {
assertSortsBefore(
`import y from "lib1";`,
`import x from "lib2";`);
});
it("Sort - relative vs relative", () => {
assertSortsBefore(
`import y from "./lib1";`,
`import x from "./lib2";`);
});
it("Sort - relative vs non-relative", () => {
assertSortsBefore(
`import y from "lib";`,
`import x from "./lib";`);
});
function assertUnaffectedBySort(...importStrings: string[]) {
const unsortedImports1 = parseImports(...importStrings);
assertListEqual(unsortedImports1, OrganizeImports.sortImports(unsortedImports1));
const unsortedImports2 = reverse(unsortedImports1);
assertListEqual(unsortedImports2, OrganizeImports.sortImports(unsortedImports2));
}
function assertSortsBefore(importString1: string, importString2: string) {
const imports = parseImports(importString1, importString2);
assertListEqual(imports, OrganizeImports.sortImports(imports));
assertListEqual(imports, OrganizeImports.sortImports(reverse(imports)));
}
});
describe("Coalesce imports", () => {
it("No imports", () => {
assert.isEmpty(OrganizeImports.coalesceImports([]));
});
it("Sort specifiers", () => {
const sortedImports = parseImports(`import { default as m, a as n, b, y, z as o } from "lib";`);
const actualCoalescedImports = OrganizeImports.coalesceImports(sortedImports);
const expectedCoalescedImports = parseImports(`import { a as n, b, default as m, y, z as o } from "lib";`);
assertListEqual(expectedCoalescedImports, actualCoalescedImports);
});
it("Combine side-effect-only imports", () => {
const sortedImports = parseImports(
`import "lib";`,
`import "lib";`);
const actualCoalescedImports = OrganizeImports.coalesceImports(sortedImports);
const expectedCoalescedImports = parseImports(`import "lib";`);
assertListEqual(expectedCoalescedImports, actualCoalescedImports);
});
it("Combine namespace imports", () => {
const sortedImports = parseImports(
`import * as x from "lib";`,
`import * as y from "lib";`);
const actualCoalescedImports = OrganizeImports.coalesceImports(sortedImports);
const expectedCoalescedImports = sortedImports;
assertListEqual(expectedCoalescedImports, actualCoalescedImports);
});
it("Combine default imports", () => {
const sortedImports = parseImports(
`import x from "lib";`,
`import y from "lib";`);
const actualCoalescedImports = OrganizeImports.coalesceImports(sortedImports);
const expectedCoalescedImports = parseImports(`import { default as x, default as y } from "lib";`);
assertListEqual(expectedCoalescedImports, actualCoalescedImports);
});
it("Combine property imports", () => {
const sortedImports = parseImports(
`import { x } from "lib";`,
`import { y as z } from "lib";`);
const actualCoalescedImports = OrganizeImports.coalesceImports(sortedImports);
const expectedCoalescedImports = parseImports(`import { x, y as z } from "lib";`);
assertListEqual(expectedCoalescedImports, actualCoalescedImports);
});
it("Combine side-effect-only import with namespace import", () => {
const sortedImports = parseImports(
`import "lib";`,
`import * as x from "lib";`);
const actualCoalescedImports = OrganizeImports.coalesceImports(sortedImports);
const expectedCoalescedImports = sortedImports;
assertListEqual(expectedCoalescedImports, actualCoalescedImports);
});
it("Combine side-effect-only import with default import", () => {
const sortedImports = parseImports(
`import "lib";`,
`import x from "lib";`);
const actualCoalescedImports = OrganizeImports.coalesceImports(sortedImports);
const expectedCoalescedImports = sortedImports;
assertListEqual(expectedCoalescedImports, actualCoalescedImports);
});
it("Combine side-effect-only import with property import", () => {
const sortedImports = parseImports(
`import "lib";`,
`import { x } from "lib";`);
const actualCoalescedImports = OrganizeImports.coalesceImports(sortedImports);
const expectedCoalescedImports = sortedImports;
assertListEqual(expectedCoalescedImports, actualCoalescedImports);
});
it("Combine namespace import with default import", () => {
const sortedImports = parseImports(
`import * as x from "lib";`,
`import y from "lib";`);
const actualCoalescedImports = OrganizeImports.coalesceImports(sortedImports);
const expectedCoalescedImports = parseImports(
`import y, * as x from "lib";`);
assertListEqual(expectedCoalescedImports, actualCoalescedImports);
});
it("Combine namespace import with property import", () => {
const sortedImports = parseImports(
`import * as x from "lib";`,
`import { y } from "lib";`);
const actualCoalescedImports = OrganizeImports.coalesceImports(sortedImports);
const expectedCoalescedImports = sortedImports;
assertListEqual(expectedCoalescedImports, actualCoalescedImports);
});
it("Combine default import with property import", () => {
const sortedImports = parseImports(
`import x from "lib";`,
`import { y } from "lib";`);
const actualCoalescedImports = OrganizeImports.coalesceImports(sortedImports);
const expectedCoalescedImports = parseImports(
`import x, { y } from "lib";`);
assertListEqual(expectedCoalescedImports, actualCoalescedImports);
});
it("Combine many imports", () => {
const sortedImports = parseImports(
`import "lib";`,
`import * as y from "lib";`,
`import w from "lib";`,
`import { b } from "lib";`,
`import "lib";`,
`import * as x from "lib";`,
`import z from "lib";`,
`import { a } from "lib";`);
const actualCoalescedImports = OrganizeImports.coalesceImports(sortedImports);
const expectedCoalescedImports = parseImports(
`import "lib";`,
`import * as x from "lib";`,
`import * as y from "lib";`,
`import { a, b, default as w, default as z } from "lib";`);
assertListEqual(expectedCoalescedImports, actualCoalescedImports);
});
it("Combine imports from different modules", () => {
const sortedImports = parseImports(
`import { d } from "lib1";`,
`import { b } from "lib1";`,
`import { c } from "lib2";`,
`import { a } from "lib2";`);
const actualCoalescedImports = OrganizeImports.coalesceImports(sortedImports);
const expectedCoalescedImports = parseImports(
`import { b, d } from "lib1";`,
`import { a, c } from "lib2";`);
assertListEqual(expectedCoalescedImports, actualCoalescedImports);
});
// This is descriptive, rather than normative
it("Combine two namespace imports with one default import", () => {
const sortedImports = parseImports(
`import * as x from "lib";`,
`import * as y from "lib";`,
`import z from "lib";`);
const actualCoalescedImports = OrganizeImports.coalesceImports(sortedImports);
const expectedCoalescedImports = sortedImports;
assertListEqual(expectedCoalescedImports, actualCoalescedImports);
});
});
describe("Baselines", () => {
const libFile = {
path: "/lib.ts",
content: `
export function F1();
export default function F2();
`,
};
testOrganizeImports("Simple",
{
path: "/test.ts",
content: `
import { F1, F2 } from "lib";
import * as NS from "lib";
import D from "lib";
NS.F1();
D();
F1();
F2();
`,
},
libFile);
testOrganizeImports("MoveToTop",
{
path: "/test.ts",
content: `
import { F1, F2 } from "lib";
F1();
F2();
import * as NS from "lib";
NS.F1();
import D from "lib";
D();
`,
},
libFile);
// tslint:disable no-invalid-template-strings
testOrganizeImports("MoveToTop_Invalid",
{
path: "/test.ts",
content: `
import { F1, F2 } from "lib";
F1();
F2();
import * as NS from "lib";
NS.F1();
import b from ${"`${'lib'}`"};
import a from ${"`${'lib'}`"};
import D from "lib";
D();
`,
},
libFile);
// tslint:enable no-invalid-template-strings
testOrganizeImports("CoalesceTrivia",
{
path: "/test.ts",
content: `
/*A*/import /*B*/ { /*C*/ F2 /*D*/ } /*E*/ from /*F*/ "lib" /*G*/;/*H*/ //I
/*J*/import /*K*/ { /*L*/ F1 /*M*/ } /*N*/ from /*O*/ "lib" /*P*/;/*Q*/ //R
F1();
F2();
`,
},
libFile);
testOrganizeImports("SortTrivia",
{
path: "/test.ts",
content: `
/*A*/import /*B*/ "lib2" /*C*/;/*D*/ //E
/*F*/import /*G*/ "lib1" /*H*/;/*I*/ //J
`,
},
{ path: "/lib1.ts", content: "" },
{ path: "/lib2.ts", content: "" });
function testOrganizeImports(testName: string, testFile: TestFSWithWatch.FileOrFolder, ...otherFiles: TestFSWithWatch.FileOrFolder[]) {
it(testName, () => runBaseline(`organizeImports/${testName}.ts`, testFile, ...otherFiles));
}
function runBaseline(baselinePath: string, testFile: TestFSWithWatch.FileOrFolder, ...otherFiles: TestFSWithWatch.FileOrFolder[]) {
const { path: testPath, content: testContent } = testFile;
const languageService = makeLanguageService(testFile, ...otherFiles);
const changes = languageService.organizeImports({ type: "file", fileName: testPath }, testFormatOptions);
assert.equal(1, changes.length);
assert.equal(testPath, changes[0].fileName);
Harness.Baseline.runBaseline(baselinePath, () => {
const newText = textChanges.applyChanges(testContent, changes[0].textChanges);
return [
"// ==ORIGINAL==",
testContent,
"// ==ORGANIZED==",
newText,
].join(newLineCharacter);
});
}
function makeLanguageService(...files: TestFSWithWatch.FileOrFolder[]) {
const host = projectSystem.createServerHost(files);
const projectService = projectSystem.createProjectService(host, { useSingleInferredProject: true });
files.forEach(f => projectService.openClientFile(f.path));
return projectService.inferredProjects[0].getLanguageService();
}
});
function parseImports(...importStrings: string[]): ReadonlyArray<ImportDeclaration> {
const sourceFile = createSourceFile("a.ts", importStrings.join("\n"), ScriptTarget.ES2015, /*setParentNodes*/ true, ScriptKind.TS);
const imports = filter(sourceFile.statements, isImportDeclaration);
assert.equal(importStrings.length, imports.length);
return imports;
}
function assertEqual(node1?: Node, node2?: Node) {
if (node1 === undefined) {
assert.isUndefined(node2);
return;
}
else if (node2 === undefined) {
assert.isUndefined(node1); // Guaranteed to fail
return;
}
assert.equal(node1.kind, node2.kind);
switch (node1.kind) {
case SyntaxKind.ImportDeclaration:
const decl1 = node1 as ImportDeclaration;
const decl2 = node2 as ImportDeclaration;
assertEqual(decl1.importClause, decl2.importClause);
assertEqual(decl1.moduleSpecifier, decl2.moduleSpecifier);
break;
case SyntaxKind.ImportClause:
const clause1 = node1 as ImportClause;
const clause2 = node2 as ImportClause;
assertEqual(clause1.name, clause2.name);
assertEqual(clause1.namedBindings, clause2.namedBindings);
break;
case SyntaxKind.NamespaceImport:
const nsi1 = node1 as NamespaceImport;
const nsi2 = node2 as NamespaceImport;
assertEqual(nsi1.name, nsi2.name);
break;
case SyntaxKind.NamedImports:
const ni1 = node1 as NamedImports;
const ni2 = node2 as NamedImports;
assertListEqual(ni1.elements, ni2.elements);
break;
case SyntaxKind.ImportSpecifier:
const is1 = node1 as ImportSpecifier;
const is2 = node2 as ImportSpecifier;
assertEqual(is1.name, is2.name);
assertEqual(is1.propertyName, is2.propertyName);
break;
case SyntaxKind.Identifier:
const id1 = node1 as Identifier;
const id2 = node2 as Identifier;
assert.equal(id1.text, id2.text);
break;
case SyntaxKind.StringLiteral:
case SyntaxKind.NoSubstitutionTemplateLiteral:
const sl1 = node1 as LiteralLikeNode;
const sl2 = node2 as LiteralLikeNode;
assert.equal(sl1.text, sl2.text);
break;
default:
assert.equal(node1.getText(), node2.getText());
break;
}
}
function assertListEqual(list1: ReadonlyArray<Node>, list2: ReadonlyArray<Node>) {
if (list1 === undefined || list2 === undefined) {
assert.isUndefined(list1);
assert.isUndefined(list2);
return;
}
assert.equal(list1.length, list2.length);
for (let i = 0; i < list1.length; i++) {
assertEqual(list1[i], list2[i]);
}
}
function reverse<T>(list: ReadonlyArray<T>) {
const result = [];
for (let i = list.length - 1; i >= 0; i--) {
result.push(list[i]);
}
return result;
}
});
}

View File

@@ -262,6 +262,8 @@ namespace ts.server {
CommandNames.GetApplicableRefactors,
CommandNames.GetEditsForRefactor,
CommandNames.GetEditsForRefactorFull,
CommandNames.OrganizeImports,
CommandNames.OrganizeImportsFull,
];
it("should not throw when commands are executed with invalid arguments", () => {

View File

@@ -629,6 +629,10 @@ namespace ts.server {
};
}
organizeImports(_scope: OrganizeImportsScope, _formatOptions: FormatCodeSettings): ReadonlyArray<FileTextChanges> {
return notImplemented();
}
private convertCodeEditsToTextChanges(edits: protocol.FileCodeEdits[]): FileTextChanges[] {
return edits.map(edit => {
const fileName = edit.fileName;

View File

@@ -113,6 +113,10 @@ namespace ts.server.protocol {
/* @internal */
GetEditsForRefactorFull = "getEditsForRefactor-full",
OrganizeImports = "organizeImports",
/* @internal */
OrganizeImportsFull = "organizeImports-full",
// NOTE: If updating this, be sure to also update `allCommandNames` in `harness/unittests/session.ts`.
}
@@ -547,6 +551,27 @@ namespace ts.server.protocol {
renameFilename?: string;
}
/**
* Organize imports by:
* 1) Removing unused imports
* 2) Coalescing imports from the same module
* 3) Sorting imports
*/
export interface OrganizeImportsRequest extends Request {
command: CommandTypes.OrganizeImports;
arguments: OrganizeImportsRequestArgs;
}
export type OrganizeImportsScope = GetCombinedCodeFixScope;
export interface OrganizeImportsRequestArgs {
scope: OrganizeImportsScope;
}
export interface OrganizeImportsResponse extends Response {
edits: ReadonlyArray<FileCodeEdits>;
}
/**
* Request for the available codefixes at a specific position.
*/

View File

@@ -1597,6 +1597,19 @@ namespace ts.server {
}
}
private organizeImports({ scope }: protocol.OrganizeImportsRequestArgs, simplifiedResult: boolean): ReadonlyArray<protocol.FileCodeEdits> | ReadonlyArray<FileTextChanges> {
Debug.assert(scope.type === "file");
const { file, project } = this.getFileAndProject(scope.args);
const formatOptions = this.projectService.getFormatCodeOptions(file);
const changes = project.getLanguageService().organizeImports({ type: "file", fileName: file }, formatOptions);
if (simplifiedResult) {
return this.mapTextChangesToCodeEdits(project, changes);
}
else {
return changes;
}
}
private getCodeFixes(args: protocol.CodeFixRequestArgs, simplifiedResult: boolean): ReadonlyArray<protocol.CodeAction> | ReadonlyArray<CodeAction> {
if (args.errorCodes.length === 0) {
return undefined;
@@ -2041,6 +2054,12 @@ namespace ts.server {
},
[CommandNames.GetEditsForRefactorFull]: (request: protocol.GetEditsForRefactorRequest) => {
return this.requiredResponse(this.getEditsForRefactor(request.arguments, /*simplifiedResult*/ false));
},
[CommandNames.OrganizeImports]: (request: protocol.OrganizeImportsRequest) => {
return this.requiredResponse(this.organizeImports(request.arguments, /*simplifiedResult*/ true));
},
[CommandNames.OrganizeImportsFull]: (request: protocol.OrganizeImportsRequest) => {
return this.requiredResponse(this.organizeImports(request.arguments, /*simplifiedResult*/ false));
}
});

View File

@@ -0,0 +1,234 @@
/* @internal */
namespace ts.OrganizeImports {
export function organizeImports(
sourceFile: SourceFile,
formatContext: formatting.FormatContext,
host: LanguageServiceHost) {
// TODO (https://github.com/Microsoft/TypeScript/issues/10020): sort *within* ambient modules (find using isAmbientModule)
// All of the old ImportDeclarations in the file, in syntactic order.
const oldImportDecls = sourceFile.statements.filter(isImportDeclaration);
if (oldImportDecls.length === 0) {
return [];
}
const oldValidImportDecls = oldImportDecls.filter(importDecl => getExternalModuleName(importDecl.moduleSpecifier));
const oldInvalidImportDecls = oldImportDecls.filter(importDecl => !getExternalModuleName(importDecl.moduleSpecifier));
// All of the new ImportDeclarations in the file, in sorted order.
const newImportDecls = coalesceImports(sortImports(removeUnusedImports(oldValidImportDecls))).concat(oldInvalidImportDecls);
const changeTracker = textChanges.ChangeTracker.fromContext({ host, formatContext });
// Delete or replace the first import.
if (newImportDecls.length === 0) {
changeTracker.deleteNode(sourceFile, oldImportDecls[0]);
}
else {
// Note: Delete the surrounding trivia because it will have been retained in newImportDecls.
changeTracker.replaceNodeWithNodes(sourceFile, oldImportDecls[0], newImportDecls, {
useNonAdjustedStartPosition: false,
useNonAdjustedEndPosition: false,
suffix: getNewLineOrDefaultFromHost(host, formatContext.options),
});
}
// Delete any subsequent imports.
for (let i = 1; i < oldImportDecls.length; i++) {
changeTracker.deleteNode(sourceFile, oldImportDecls[i]);
}
return changeTracker.getChanges();
}
function removeUnusedImports(oldImports: ReadonlyArray<ImportDeclaration>) {
return oldImports; // TODO (https://github.com/Microsoft/TypeScript/issues/10020)
}
/* @internal */ // Internal for testing
export function sortImports(oldImports: ReadonlyArray<ImportDeclaration>) {
return stableSort(oldImports, (import1, import2) => {
const name1 = getExternalModuleName(import1.moduleSpecifier);
const name2 = getExternalModuleName(import2.moduleSpecifier);
Debug.assert(name1 !== undefined);
Debug.assert(name2 !== undefined);
return compareBooleans(isExternalModuleNameRelative(name1), isExternalModuleNameRelative(name2)) ||
compareStringsCaseSensitive(name1, name2);
});
}
function getExternalModuleName(specifier: Expression) {
return isStringLiteral(specifier) || isNoSubstitutionTemplateLiteral(specifier)
? specifier.text
: undefined;
}
/**
* @param sortedImports a non-empty list of ImportDeclarations, sorted by module name.
*/
function groupSortedImports(sortedImports: ReadonlyArray<ImportDeclaration>): ReadonlyArray<ReadonlyArray<ImportDeclaration>> {
Debug.assert(length(sortedImports) > 0);
const groups: ImportDeclaration[][] = [];
let groupName: string | undefined = getExternalModuleName(sortedImports[0].moduleSpecifier);
Debug.assert(groupName !== undefined);
let group: ImportDeclaration[] = [];
for (const importDeclaration of sortedImports) {
const moduleName = getExternalModuleName(importDeclaration.moduleSpecifier);
Debug.assert(moduleName !== undefined);
if (moduleName === groupName) {
group.push(importDeclaration);
}
else if (group.length) {
groups.push(group);
groupName = moduleName;
group = [importDeclaration];
}
}
if (group.length) {
groups.push(group);
}
return groups;
}
/* @internal */ // Internal for testing
/**
* @param sortedImports a list of ImportDeclarations, sorted by module name.
*/
export function coalesceImports(sortedImports: ReadonlyArray<ImportDeclaration>) {
if (sortedImports.length === 0) {
return sortedImports;
}
const coalescedImports: ImportDeclaration[] = [];
const groupedImports = groupSortedImports(sortedImports);
for (const importGroup of groupedImports) {
const { importWithoutClause, defaultImports, namespaceImports, namedImports } = getImportParts(importGroup);
if (importWithoutClause) {
coalescedImports.push(importWithoutClause);
}
// Normally, we don't combine default and namespace imports, but it would be silly to
// produce two import declarations in this special case.
if (defaultImports.length === 1 && namespaceImports.length === 1 && namedImports.length === 0) {
// Add the namespace import to the existing default ImportDeclaration.
const defaultImportClause = defaultImports[0].parent as ImportClause;
coalescedImports.push(
updateImportDeclarationAndClause(defaultImportClause, defaultImportClause.name, namespaceImports[0]));
continue;
}
const sortedNamespaceImports = stableSort(namespaceImports, (n1, n2) => compareIdentifiers(n1.name, n2.name));
for (const namespaceImport of sortedNamespaceImports) {
// Drop the name, if any
coalescedImports.push(
updateImportDeclarationAndClause(namespaceImport.parent, /*name*/ undefined, namespaceImport));
}
if (defaultImports.length === 0 && namedImports.length === 0) {
continue;
}
let newDefaultImport: Identifier | undefined;
const newImportSpecifiers: ImportSpecifier[] = [];
if (defaultImports.length === 1) {
newDefaultImport = defaultImports[0];
}
else {
for (const defaultImport of defaultImports) {
newImportSpecifiers.push(
createImportSpecifier(createIdentifier("default"), defaultImport));
}
}
newImportSpecifiers.push(...flatMap(namedImports, n => n.elements));
const sortedImportSpecifiers = stableSort(newImportSpecifiers, (s1, s2) =>
compareIdentifiers(s1.propertyName || s1.name, s2.propertyName || s2.name) ||
compareIdentifiers(s1.name, s2.name));
const importClause = defaultImports.length > 0
? defaultImports[0].parent as ImportClause
: namedImports[0].parent;
const newNamedImports = sortedImportSpecifiers.length === 0
? undefined
: namedImports.length === 0
? createNamedImports(sortedImportSpecifiers)
: updateNamedImports(namedImports[0], sortedImportSpecifiers);
coalescedImports.push(
updateImportDeclarationAndClause(importClause, newDefaultImport, newNamedImports));
}
return coalescedImports;
function getImportParts(importGroup: ReadonlyArray<ImportDeclaration>) {
let importWithoutClause: ImportDeclaration | undefined;
const defaultImports: Identifier[] = [];
const namespaceImports: NamespaceImport[] = [];
const namedImports: NamedImports[] = [];
for (const importDeclaration of importGroup) {
if (importDeclaration.importClause === undefined) {
// Only the first such import is interesting - the others are redundant.
// Note: Unfortunately, we will lose trivia that was on this node.
importWithoutClause = importWithoutClause || importDeclaration;
continue;
}
const { name, namedBindings } = importDeclaration.importClause;
if (name) {
defaultImports.push(name);
}
if (namedBindings) {
if (isNamespaceImport(namedBindings)) {
namespaceImports.push(namedBindings);
}
else {
namedImports.push(namedBindings);
}
}
}
return {
importWithoutClause,
defaultImports,
namespaceImports,
namedImports,
};
}
function compareIdentifiers(s1: Identifier, s2: Identifier) {
return compareStringsCaseSensitive(s1.text, s2.text);
}
function updateImportDeclarationAndClause(
importClause: ImportClause,
name: Identifier | undefined,
namedBindings: NamedImportBindings | undefined) {
const importDeclaration = importClause.parent;
return updateImportDeclaration(
importDeclaration,
importDeclaration.decorators,
importDeclaration.modifiers,
updateImportClause(importClause, name, namedBindings),
importDeclaration.moduleSpecifier);
}
}
}

View File

@@ -14,6 +14,7 @@
/// <reference path='jsTyping.ts' />
/// <reference path='navigateTo.ts' />
/// <reference path='navigationBar.ts' />
/// <reference path='organizeImports.ts' />
/// <reference path='outliningElementsCollector.ts' />
/// <reference path='patternMatcher.ts' />
/// <reference path='preProcess.ts' />
@@ -1848,6 +1849,15 @@ namespace ts {
return codefix.getAllFixes({ fixId, sourceFile, program, host, cancellationToken, formatContext });
}
function organizeImports(scope: OrganizeImportsScope, formatOptions: FormatCodeSettings): ReadonlyArray<FileTextChanges> {
synchronizeHostData();
Debug.assert(scope.type === "file");
const sourceFile = getValidSourceFile(scope.fileName);
const formatContext = formatting.getFormatContext(formatOptions);
return OrganizeImports.organizeImports(sourceFile, formatContext, host);
}
function applyCodeActionCommand(action: CodeActionCommand): Promise<ApplyCodeActionCommandResult>;
function applyCodeActionCommand(action: CodeActionCommand[]): Promise<ApplyCodeActionCommandResult[]>;
function applyCodeActionCommand(action: CodeActionCommand | CodeActionCommand[]): Promise<ApplyCodeActionCommandResult | ApplyCodeActionCommandResult[]>;
@@ -2143,6 +2153,7 @@ namespace ts {
getCodeFixesAtPosition,
getCombinedCodeFix,
applyCodeActionCommand,
organizeImports,
getEmitOutput,
getNonBoundSourceFile,
getSourceFile,

View File

@@ -58,6 +58,7 @@
"jsTyping.ts",
"navigateTo.ts",
"navigationBar.ts",
"organizeImports.ts",
"outliningElementsCollector.ts",
"pathCompletions.ts",
"patternMatcher.ts",

View File

@@ -308,6 +308,7 @@ namespace ts {
applyCodeActionCommand(fileName: string, action: CodeActionCommand | CodeActionCommand[]): Promise<ApplyCodeActionCommandResult | ApplyCodeActionCommandResult[]>;
getApplicableRefactors(fileName: string, positionOrRaneg: number | TextRange): ApplicableRefactorInfo[];
getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string): RefactorEditInfo | undefined;
organizeImports(scope: OrganizeImportsScope, formatOptions: FormatCodeSettings): ReadonlyArray<FileTextChanges>;
getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean): EmitOutput;
@@ -326,6 +327,8 @@ namespace ts {
export interface CombinedCodeFixScope { type: "file"; fileName: string; }
export type OrganizeImportsScope = CombinedCodeFixScope;
export interface GetCompletionsAtPositionOptions {
includeExternalModuleExports: boolean;
includeInsertTextCompletions: boolean;

View File

@@ -4135,6 +4135,7 @@ declare namespace ts {
applyCodeActionCommand(fileName: string, action: CodeActionCommand | CodeActionCommand[]): Promise<ApplyCodeActionCommandResult | ApplyCodeActionCommandResult[]>;
getApplicableRefactors(fileName: string, positionOrRaneg: number | TextRange): ApplicableRefactorInfo[];
getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string): RefactorEditInfo | undefined;
organizeImports(scope: OrganizeImportsScope, formatOptions: FormatCodeSettings): ReadonlyArray<FileTextChanges>;
getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean): EmitOutput;
getProgram(): Program;
dispose(): void;
@@ -4143,6 +4144,7 @@ declare namespace ts {
type: "file";
fileName: string;
}
type OrganizeImportsScope = CombinedCodeFixScope;
interface GetCompletionsAtPositionOptions {
includeExternalModuleExports: boolean;
includeInsertTextCompletions: boolean;
@@ -5078,6 +5080,7 @@ declare namespace ts.server.protocol {
GetSupportedCodeFixes = "getSupportedCodeFixes",
GetApplicableRefactors = "getApplicableRefactors",
GetEditsForRefactor = "getEditsForRefactor",
OrganizeImports = "organizeImports",
}
/**
* A TypeScript Server message
@@ -5429,6 +5432,23 @@ declare namespace ts.server.protocol {
renameLocation?: Location;
renameFilename?: string;
}
/**
* Organize imports by:
* 1) Removing unused imports
* 2) Coalescing imports from the same module
* 3) Sorting imports
*/
interface OrganizeImportsRequest extends Request {
command: CommandTypes.OrganizeImports;
arguments: OrganizeImportsRequestArgs;
}
type OrganizeImportsScope = GetCombinedCodeFixScope;
interface OrganizeImportsRequestArgs {
scope: OrganizeImportsScope;
}
interface OrganizeImportsResponse extends Response {
edits: ReadonlyArray<FileCodeEdits>;
}
/**
* Request for the available codefixes at a specific position.
*/
@@ -7282,6 +7302,7 @@ declare namespace ts.server {
private extractPositionAndRange(args, scriptInfo);
private getApplicableRefactors(args);
private getEditsForRefactor(args, simplifiedResult);
private organizeImports({scope}, simplifiedResult);
private getCodeFixes(args, simplifiedResult);
private getCombinedCodeFix({scope, fixId}, simplifiedResult);
private applyCodeActionCommand(args);

View File

@@ -4387,6 +4387,7 @@ declare namespace ts {
applyCodeActionCommand(fileName: string, action: CodeActionCommand | CodeActionCommand[]): Promise<ApplyCodeActionCommandResult | ApplyCodeActionCommandResult[]>;
getApplicableRefactors(fileName: string, positionOrRaneg: number | TextRange): ApplicableRefactorInfo[];
getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string): RefactorEditInfo | undefined;
organizeImports(scope: OrganizeImportsScope, formatOptions: FormatCodeSettings): ReadonlyArray<FileTextChanges>;
getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean): EmitOutput;
getProgram(): Program;
dispose(): void;
@@ -4395,6 +4396,7 @@ declare namespace ts {
type: "file";
fileName: string;
}
type OrganizeImportsScope = CombinedCodeFixScope;
interface GetCompletionsAtPositionOptions {
includeExternalModuleExports: boolean;
includeInsertTextCompletions: boolean;

View File

@@ -0,0 +1,15 @@
// ==ORIGINAL==
/*A*/import /*B*/ { /*C*/ F2 /*D*/ } /*E*/ from /*F*/ "lib" /*G*/;/*H*/ //I
/*J*/import /*K*/ { /*L*/ F1 /*M*/ } /*N*/ from /*O*/ "lib" /*P*/;/*Q*/ //R
F1();
F2();
// ==ORGANIZED==
/*A*/ import { /*L*/ F1 /*M*/, /*C*/ F2 /*D*/ } /*E*/ from "lib" /*G*/; /*H*/ //I
F1();
F2();

View File

@@ -0,0 +1,18 @@
// ==ORIGINAL==
import { F1, F2 } from "lib";
F1();
F2();
import * as NS from "lib";
NS.F1();
import D from "lib";
D();
// ==ORGANIZED==
import * as NS from "lib";
import D, { F1, F2 } from "lib";
F1();
F2();
NS.F1();
D();

View File

@@ -0,0 +1,22 @@
// ==ORIGINAL==
import { F1, F2 } from "lib";
F1();
F2();
import * as NS from "lib";
NS.F1();
import b from `${'lib'}`;
import a from `${'lib'}`;
import D from "lib";
D();
// ==ORGANIZED==
import * as NS from "lib";
import D, { F1, F2 } from "lib";
import b from `${'lib'}`;
import a from `${'lib'}`;
F1();
F2();
NS.F1();
D();

View File

@@ -0,0 +1,20 @@
// ==ORIGINAL==
import { F1, F2 } from "lib";
import * as NS from "lib";
import D from "lib";
NS.F1();
D();
F1();
F2();
// ==ORGANIZED==
import * as NS from "lib";
import D, { F1, F2 } from "lib";
NS.F1();
D();
F1();
F2();

View File

@@ -0,0 +1,10 @@
// ==ORIGINAL==
/*A*/import /*B*/ "lib2" /*C*/;/*D*/ //E
/*F*/import /*G*/ "lib1" /*H*/;/*I*/ //J
// ==ORGANIZED==
/*F*/ import "lib1" /*H*/; /*I*/ //J
/*A*/ import "lib2" /*C*/; /*D*/ //E