mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-05-09 07:55:10 -05:00
Merge pull request #21909 from amcasey/OrganizeImports
Introduce an organizeImports command
This commit is contained in:
@@ -141,6 +141,7 @@ var harnessSources = harnessCoreSources.concat([
|
||||
"typingsInstaller.ts",
|
||||
"projectErrors.ts",
|
||||
"matchFiles.ts",
|
||||
"organizeImports.ts",
|
||||
"initializeTSConfig.ts",
|
||||
"extractConstants.ts",
|
||||
"extractFunctions.ts",
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
426
src/harness/unittests/organizeImports.ts
Normal file
426
src/harness/unittests/organizeImports.ts
Normal 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;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
234
src/services/organizeImports.ts
Normal file
234
src/services/organizeImports.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
"jsTyping.ts",
|
||||
"navigateTo.ts",
|
||||
"navigationBar.ts",
|
||||
"organizeImports.ts",
|
||||
"outliningElementsCollector.ts",
|
||||
"pathCompletions.ts",
|
||||
"patternMatcher.ts",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
15
tests/baselines/reference/organizeImports/CoalesceTrivia.ts
Normal file
15
tests/baselines/reference/organizeImports/CoalesceTrivia.ts
Normal 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();
|
||||
18
tests/baselines/reference/organizeImports/MoveToTop.ts
Normal file
18
tests/baselines/reference/organizeImports/MoveToTop.ts
Normal 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();
|
||||
@@ -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();
|
||||
20
tests/baselines/reference/organizeImports/Simple.ts
Normal file
20
tests/baselines/reference/organizeImports/Simple.ts
Normal 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();
|
||||
10
tests/baselines/reference/organizeImports/SortTrivia.ts
Normal file
10
tests/baselines/reference/organizeImports/SortTrivia.ts
Normal 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
|
||||
|
||||
Reference in New Issue
Block a user